Синтаксис функции, не являющейся членом, не являющейся другом

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

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

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

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

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

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

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

std::string s("foobar");

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

Редактировать:

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

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

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

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

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

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

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


person mmocny    schedule 01.12.2008    source источник


Ответы (7)


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

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

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

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

person Andreas Magnusson    schedule 02.12.2008
comment
мультиметоды в С++ - я не знал, что здесь ведется работа. Спасибо за внимание! Надеюсь, они добьются хорошего прогресса. - person mmocny; 03.12.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 ;
}

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

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

Изменить: некоторые примеры

Допустим, я хочу по какой-то причине создать два класса, подобных Integer. Первым будет 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 ‹windows.h›), вы можете это сделать, не изменяя сама структура. Для оператора синтаксис будет естественным, а для неоператора ну...

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

person paercebal    schedule 01.12.2008
comment
Стоит отметить, что существует семантическая разница между бесплатными функциями и функциями-членами — функции-члены никогда не будут выполнять никаких преобразований для объекта this, за исключением пары неявных преобразований. Сюда входят операторы, и именно поэтому такие операторы, как +, всегда должны быть свободными функциями. - person coppro; 02.12.2008
comment
Предполагая, что вы хотите неявное преобразование с помощью операторов, т.е. Если вы не определили соответствующий конструктор с 1 аргументом и оператор приведения, скорее всего, вы этого не сделаете, поэтому, на самом деле, лучше сделать любые другие операторы функциями-членами. - person Steve Jessop; 02.12.2008
comment
Отличный ответ, однако, интересно, не могли бы вы добавить немного больше о конкретном вопросе, касающемся фактического вызова объявленных функций (член или не-член, не-друг), особенно когда оба имеют одно и то же имя и функциональность. Спасибо. - person mmocny; 02.12.2008

Нет возможности написать не член не друг через точку, а именно потому что "оператор". не может быть перегружен.

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

person grepsedawk    schedule 01.12.2008

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

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

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

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

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

person Konrad Rudolph    schedule 01.12.2008
comment
RE: группировка пространства имен: я знал это, но хотел, чтобы вопрос был кратким, фактическая предпосылка и совет статьи здесь не обсуждались. - person mmocny; 02.12.2008
comment
RE: собственно вопрос: я согласен с вами по поводу заголовка ‹algorithm›. Однако я не думаю, что мы сравниваем здесь одни и те же вещи. Я думаю, что естественно говорить sort( Container.begin(), Container.end() ), а не Container.sort().. - person mmocny; 02.12.2008
comment
.. но я не думаю, что естественно иногда говорить String.insert(...) и вставлять(String, ...) во время других. - person mmocny; 02.12.2008
comment
Если вы сами разрабатываете интерфейс, вы можете вместо этого сделать этот метод insert() (тот, который знает внутренности вашего класса) другом. Затем вы должны написать вставить (String, ...) во всех случаях. - person Andreas Magnusson; 02.12.2008
comment
@Andreas Magnusson: Думаю, смысл был в том, чтобы не добавлять друзей в класс... ^_^ - person paercebal; 11.12.2008
comment
@Андреас Магнуссон: такое простое решение, но такое точное. Именно это я считаю правильным ответом. @paercebal: Да, я думаю, в этом и был смысл статьи, но статья пыталась переварить код из реальной жизни, а код из реальной жизни должен быть максимально удобным для использования. Возможно, мне нужно перечитать статью. - person mmocny; 16.06.2009

Лично мне нравится расширяемость бесплатных функций. Функция 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 слишком.

person Johannes Schaub - litb    schedule 02.12.2008

Если вы хотите сохранить точечную нотацию, а также отделить функции, которые не должны быть друзьями вне класса (чтобы они не могли получить доступ к закрытым членам, тем самым нарушив инкапсуляцию), вы, вероятно, могли бы написать класс-миксин. Либо сделайте "обычную" вставку чистой виртуальной в миксин, либо оставьте ее не виртуальной и используйте 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.

person Steve Jessop    schedule 02.12.2008
comment
Это, конечно, уродливо, но +1 за фантастический ответ, который обеспечивает обратное решение для того, чтобы сделать все методы не членами (сделав одну функцию другом). Я не думаю, что буду использовать этот метод в ближайшее время, но если бы примеси было проще писать, возможно, это было бы правильно. Спасибо! - person mmocny; 23.10.2009

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

person Filip Frącz    schedule 01.12.2008
comment
Global считается плохим, но не обязательно. Лично я настоятельно рекомендую использовать пространства имен. - person Filip Frącz; 02.12.2008
comment
Бесплатные функции не имеют ничего общего с глобальными переменными или глобальным состоянием (что, как правило, плохо). Нет ничего объектно-ориентированного в том, чтобы сделать функцию членом класса, если в этом нет необходимости. Бесплатные функции, как правило, лучше ООП, особенно в C++, где это обычно используется. - person jalf; 02.12.2008
comment
Да, но глобальные функции загрязняют глобальное пространство имен. Это плохо, в общем. - person Max Lybbert; 02.12.2008
comment
вот почему вы не должны делать их глобальными. Поместите их в определенное пользователем пространство имен. - person Johannes Schaub - litb; 08.09.2009