Синтаксис функции, не являющейся членом - PullRequest
5 голосов
/ 02 декабря 2008

Является ли способ использовать функцию, не являющуюся членом, не являющейся другом, в объекте, используя ту же «точечную» запись, что и функции-члены?

Могу ли я вытащить (любого) члена из класса, и чтобы пользователи использовали его так же, как всегда?

Более длинное объяснение:

Скотт Мейерс , Херб Саттер и др. Утверждают, что функции, не являющиеся членами-не-друзьями, являются частью интерфейса объекта и могут улучшить инкапсуляцию. Я согласен с ними.

Однако, после недавнего прочтения этой статьи: http://www.gotw.ca/gotw/084.htm Я задаюсь вопросом о синтаксических последствиях.

В этой статье Херб предлагает иметь один insert, erase и replace член, а также несколько не принадлежащих друг другу функций, не являющихся друзьями, с тем же именем.

Означает ли это, как я думаю, что Херб считает, что некоторые функции следует использовать с точечной нотацией, а другие - как глобальную функцию?

std::string s("foobar");

s.insert( ... ); /* One like this */
insert( s , ...); /* Others like this */

Edit:

Спасибо всем за ваши очень полезные ответы, однако, я думаю, что вопрос моего вопроса был упущен.

Я специально не упомянул конкретный случай операторов и то, как они сохраняют "естественные" обозначения. Не то, что вы должны обернуть все в пространство имен. Эти вещи написаны в статье, на которую я ссылаюсь.

Сам вопрос был:

В этой статье Херб предлагает, чтобы один метод insert () был членом, а остальные - функциями, не являющимися членами.

Это означает, что для использования одной формы вставки () необходимо использовать точечную запись, а для других - нет.

Это только у меня, или это звучит безумно?

У меня есть догадка, что, возможно, вы можете использовать один синтаксис. (Я думаю, что Boost :: function может * принять этот параметр для mem_fun).

Ответы [ 7 ]

4 голосов
/ 02 декабря 2008

Да, это означает, что часть интерфейса объекта состоит из функций, не являющихся членами.

И вы правы в том, что в нем используется следующая запись для объекта класса T:

void T::doSomething(int value) ;     // method
void doSomething(T & t, int value) ; // non-member non-friend function

Если вы хотите, чтобы функция / метод doSomething возвращали void и имели параметр int с именем "value".

Но стоит упомянуть две вещи.

Во-первых, функциональная часть интерфейса класса должна находиться в одном и том же пространстве имен. Это еще одна причина (если нужна была другая причина) использовать пространства имен, только для того, чтобы «соединить» объект и функции, являющиеся частью его интерфейса.

Хорошая часть заключается в том, что он способствует хорошей инкапсуляции. Но плохая часть в том, что он использует функциональную нотацию, которая мне лично не нравится.

Во-вторых, операторы не подпадают под это ограничение. Например, оператор + = для класса T можно записать двумя способами:

T & operator += (T & lhs, const T & rhs) ;
{
   // do something like lhs.value += rhs.value
   return lhs ;
}

T & T::operator += (const T & rhs) ;
{
   // do something like this->value += rhs.value
   return *this ;
}

Но обе записи используются как:

void doSomething(T & a, T & b)
{
   a += b ;
}

, что с эстетической точки зрения намного лучше, чем функциональная запись.

Теперь, это был бы очень крутой синтаксический сахар, чтобы иметь возможность написать функцию из того же интерфейса и при этом иметь возможность вызывать ее через "." обозначение, как в C #, как упомянуто michalmocny.

Редактировать: Некоторые примеры

Допустим, по какой-то причине я хочу создать два "целочисленных" класса. Первым будет IntegerMethod:

class IntegerMethod
{
   public :
      IntegerMethod(const int p_iValue) : m_iValue(p_iValue) {}
      int getValue() const { return this->m_iValue ; }
      void setValue(const int p_iValue) { this->m_iValue = p_iValue ; }

      IntegerMethod & operator += (const IntegerMethod & rhs)
      {
         this->m_iValue += rhs.getValue() ;
         return *this ;
      }

      IntegerMethod operator + (const IntegerMethod & rhs) const
      {
         return IntegerMethod (this->m_iValue + rhs.getValue()) ;
      }

      std::string toString() const
      {
         std::stringstream oStr ;
         oStr << this->m_iValue ;
         return oStr.str() ;
      }

   private :
      int m_iValue ;
} ;

Этот класс имеет 6 методов, которые могут получить доступ к его внутренним элементам.

Вторым является IntegerFunction:

class IntegerFunction
{
   public :
      IntegerFunction(const int p_iValue) : m_iValue(p_iValue) {}
      int getValue() const { return this->m_iValue ; }
      void setValue(const int p_iValue) { this->m_iValue = p_iValue ; }

   private :
      int m_iValue ;
} ;

IntegerFunction & operator += (IntegerFunction & lhs, const IntegerFunction & rhs)
{
   lhs.setValue(lhs.getValue() + rhs.getValue()) ;
   return lhs ;
}

IntegerFunction operator + (const IntegerFunction & lhs, const IntegerFunction & rhs)
{
   return IntegerFunction(lhs.getValue() + rhs.getValue()) ;
}

std::string toString(const IntegerFunction & p_oInteger)
{
   std::stringstream oStr ;
   oStr << p_oInteger.getValue() ;
   return oStr.str() ;
}

Он имеет только 3 метода и, таким образом, уменьшает количество кода, который может получить доступ к его внутренним компонентам. Имеет 3 функции, не являющиеся друзьями.

Два класса могут использоваться как:

void doSomething()
{
   {
      IntegerMethod iMethod(25) ;
      iMethod += 35 ;
      std::cout << "iMethod   : " << iMethod.toString() << std::endl ;

      IntegerMethod result(0), lhs(10), rhs(20) ;
      result = lhs + 20 ;
      // result = 10 + rhs ; // WON'T COMPILE
      result = 10 + 20 ;
      result = lhs + rhs ;
   }

   {
      IntegerFunction iFunction(125) ;
      iFunction += 135 ;
      std::cout << "iFunction : " << toString(iFunction) << std::endl ;

      IntegerFunction result(0), lhs(10), rhs(20) ;
      result = lhs + 20 ;
      result = 10 + rhs ;
      result = 10 + 20 ;
      result = lhs + rhs ;
   }
}

Когда мы сравниваем использование оператора ("+" и "+ ="), мы видим, что включение оператора в член или не член не имеет различий в его видимом использовании. Тем не менее, есть два различия:

  1. член имеет доступ ко всем своим внутренним элементам. Нечлены должны использовать открытые методы-члены

  2. Из некоторых бинарных операторов, таких как +, *, интересно иметь продвижение типов, потому что в одном случае (например, продвижение lhs, как показано выше) оно не будет работать для метода-члена.

Теперь, если мы сравним неоператорное использование ("toString"), мы увидим, что неоператорское использование является более "естественным" для Java-подобных разработчиков, чем не-членская функция. Несмотря на эту незнакомость, для C ++ важно признать, что, несмотря на его синтаксис, версия, не являющаяся членом, лучше с точки зрения ООП, поскольку она не имеет доступа к внутренним компонентам класса.

В качестве бонуса: если вы хотите добавить оператор (или, соответственно, неоператорную функцию) к объекту, у которого его нет (например, структура GUID ), то вы можете без необходимости изменить саму структуру. Для оператора синтаксис будет естественным, а для неоператора, ну ...

Отказ от ответственности: Конечно, эти классы глупы: set / getValue - это почти прямой доступ к его внутренним компонентам. Но замените Integer на String, как предложено Хербом Саттером в Monoliths «Unstrung» , и вы увидите более реальный случай.

1 голос
/ 02 декабря 2008

Если вы хотите сохранить точечную запись, но также и отдельные функции, которым не нужно быть друзьями вне класса (чтобы они не могли получить доступ к закрытым членам, тем самым нарушая инкапсуляцию), вы, вероятно, могли бы написать класс mixin. Либо сделайте «обычную» вставку чисто виртуальной в миксин, либо оставьте ее не виртуальной и используйте CRTP:

template<typename DERIVED, typename T>
struct OtherInsertFunctions {
    void insertUpsideDown(T t) {
        DERIVED *self = static_cast<DERIVED*>(this);
        self->insert(t.turnUpsideDown());
    }
    void insertBackToFront(T t) // etc.
    void insert(T t, Orientation o) // this one is tricksy because it's an
                                    // overload, so requires the 'using' declaration
};

template<typename T>
class MyCollection : public OtherInsertFunctions<MyCollection,T> {
public:
    // using declaration, to prevent hiding of base class overloads
    using OtherInsertFunctions<MyCollection,T>::insert;
    void insert(T t);
    // and the rest of the class goes here
};

Что-то в этом роде. Но, как уже говорили другие, программисты на C ++ не «должны» возражать против свободных функций, потому что вы «должны» всегда искать способы написания универсальных алгоритмов (например, std :: sort), а не добавлять функции-члены в конкретные занятия. Делать все последовательно методом - это больше Java-у.

1 голос
/ 02 декабря 2008

Вы можете использовать один синтаксис, но, возможно, не тот, который вам нравится. Вместо того, чтобы помещать один insert () в область видимости вашего класса, вы делаете его своим другом. Теперь вы можете написать

mystring s;
insert(s, "hello");
insert(s, other_s.begin(), other_s.end());
insert(s, 10, '.');

Для любого не виртуального, открытого метода эквивалентно определять его как функцию, не являющуюся другом. Если вам мешает смешанный синтаксис точка / точка без точки, тогда все эти методы должны стать дружественными функциями. Там нет разницы.

В будущем мы также сможем писать полиморфные функции, подобные этой, поэтому, возможно, это и есть способ C ++, а не искусственная попытка заставить свободные функции использовать синтаксис точки.

1 голос
/ 02 декабря 2008

Лично мне нравится расширяемость бесплатных функций. Функция size является отличным примером для этого:

// joe writes this container class:
namespace mylib {
    class container { 
        // ... loads of stuff ...
    public:
        std::size_t size() const { 
            // do something and return
        }
    };

    std::size_t size(container const& c) {
        return c.size();
    } 
}

// another programmer decides to write another container...
namespace bar {
    class container {
        // again, lots of stuff...
    public:
        std::size_t size() const {
            // do something and return
        }
    };

    std::size_t size(container const& c) {
        return c.size();
    } 
}

// we want to get the size of arrays too
template<typename T, std::size_t n>
std::size_t size(T (&)[n]) {
    return n;
}

Рассмотрим теперь код, который использует функцию свободного размера:

int main() {
    mylib::container c;
    std::size_t c_size = size(c);

    char data[] = "some string";
    std::size_t data_size = size(data);
}

Как видите, вы можете просто использовать size(object), не заботясь о пространстве имен, в котором находится тип (в зависимости от типа аргумента, компилятор сам определяет пространство имен), и не заботясь о том, что происходит за сцены. Рассмотрим также использование как begin и end в качестве свободной функции. Это именно то, что boost::range делает тоже.

1 голос
/ 02 декабря 2008

Однако, после недавнего прочтения этой статьи: http://www.gotw.ca/gotw/084.htm Я задаюсь вопросом о синтаксических последствиях.

Синтаксические последствия - это то, что можно увидеть повсюду в хорошо написанных библиотеках C ++: C ++ везде использует бесплатные функции. Это необычно для людей с опытом работы в ООП, но это лучшая практика в C ++. В качестве примера рассмотрим заголовок STL <algorithm>.

Таким образом, использование точечной нотации становится исключением из правила, а не наоборот.

Обратите внимание, что другие языки выбирают другие методы; это привело к появлению «методов расширения» в C # и VB, которые позволяют эмулировать синтаксис вызова метода для статических функций (то есть именно то, что вы имели в виду). С другой стороны, C # и VB являются строго объектно-ориентированными языками, поэтому более важно иметь единую запись для вызовов методов.

Кроме того, функции всегда принадлежат пространству имен - хотя я сам иногда нарушаю это правило (но только в одной единице компиляции, а именно в моем эквиваленте main.cpp, где это не играет роль).

1 голос
/ 02 декабря 2008

Не существует способа написать не член, не являющийся другом, с точечной нотацией, а именно потому, что «оператор». не может быть перегружен.

Вы должны всегда заключать классы, не являющиеся членами, не являющимися друзьями, либо в анонимное пространство имен (если функции нужны только текущему модулю перевода), либо в какое-то значимое пространство имен для пользователей.

0 голосов
/ 02 декабря 2008

Да, они должны быть глобальными или иметь пространство имен. Функции, не являющиеся членами-не-друзьями, выглядят намного красивее в C #, где они используют точечную запись (они называются методы расширения ).

...