Механика расширения через бесплатные функции или функции-члены

Множество библиотек C++, включая стандартные, позволяют адаптировать объекты для использования в библиотеках. Часто приходится выбирать между функцией-членом или свободной функцией в том же пространстве имен.

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

if Class A has member function with signature FunctionSignature
    choose &A.functionSignature(...)
else if NamespaceOfClassA has free function freeFunctionSignature
    choose freeFunctionSignature(...)
else
    throw "no valid extension function was provided"

Приведенный выше код выглядит как код среды выполнения :/. Итак, как библиотека определяет пространство имен, в котором находится класс, как она определяет три условия, каких еще ловушек следует избегать.

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

!!ЧТОБЫ ВЫИГРАТЬ НАГРАЖДЕНИЕ!!

Итак, согласно ответу Стива (и комментариям), ADL и SFINAE являются ключевыми конструкциями для подключения отправки во время компиляции. У меня есть голова вокруг ADL (примитивно) и SFINAE (опять же в зачаточном состоянии). Но я не знаю, как они организуются вместе так, как я думаю, они должны.

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

Допустим, рассматриваемый объект называется NS::Car, и этот объект должен обеспечивать поведение MoveForward(int units) в качестве функции-члена c. Если поведение должно быть получено из пространства имен объекта, оно, вероятно, будет выглядеть как MoveForward(const Car & car_, int units). Давайте определим функцию, которая хочет отправить mover(NS::direction d, const NS::vehicle & v_) , где direction — это перечисление, а v_ — это базовый класс NS::car.


person Hassan Syed    schedule 14.03.2011    source источник
comment
Вы не можете переопределить operator<< в своем классе для вывода в поток. Оператор-член должен иметь класс слева от оператора, а не справа. Кроме того, поиск имени является частью компилятора, а не библиотеки, и будет включать шаблоны только в том случае, если задействован класс шаблона или функция шаблона. Если вы спрашиваете, как выполняется поиск имени, уточните свой вопрос. В противном случае, я не знаю, о чем вы спрашиваете.   -  person David Thornley    schedule 14.03.2011
comment
@david Да, я ошибался насчет operator<<. Я удалил пример, но кто-то редактировал мой вопрос и перезаписал удаление :D   -  person Hassan Syed    schedule 14.03.2011


Ответы (5)


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

Обнаружение существования функций-членов во время компиляции

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

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

namespace your_ns {

template <class T>
void your_function(T const& t)
{
    the_operation(t); // unqualified call to free function
}

// in the same namespace, you provide the "default"
// for the_operation as a template, and have it call the member function:

template <class T>
void the_operation(T const& t)
{
    t.the_operation();
}

} // namespace your_ns

Таким образом, пользователь может предоставить свою собственную перегрузку "the_operation" в том же пространстве имен, что и его класс, чтобы он был найден ADL. Конечно, пользовательская "the_operation" должна быть "более специализированной", чем ваша реализация по умолчанию, иначе вызов будет неоднозначным. На практике это не проблема, поскольку все, что ограничивает тип параметра больше, чем ссылка на константу что-либо, будет "более специализированным".

Пример:

namespace users_ns {

class foo {};

void the_operation(foo const& f)
{
    std::cout << "foo\n";
}

template <class T>
class bar {};

template <class T>
void the_operation(bar<T> const& b)
{
    std::cout << "bar\n";
}

} // namespace users_ns

РЕДАКТИРОВАТЬ: еще раз прочитав ответ Стива Джессопа, я понимаю, что это в основном то, что он написал, только с большим количеством слов :)

person Paul Groke    schedule 24.03.2011
comment
Больше слов и явная демонстрация концепции в коде. +1 за это - person Phil Miller; 24.03.2011
comment
спасибо, приятель, меня действительно интересовал случай статического типа. наблюдение за написанным кодом вызывает эффект ага :D - person Hassan Syed; 29.03.2011

Библиотека не делает ничего из этого во время выполнения, отправка выполняется компилятором при компиляции вызывающего кода. Свободные функции в том же пространстве имен, что и один из аргументов, находятся в соответствии с правилами механизма, называемого «поиск в зависимости от аргумента» (ADL), иногда называемого «поиск Кенига».

В тех случаях, когда у вас есть возможность реализовать либо бесплатную функцию, либо функцию-член, это может быть связано с тем, что библиотека предоставляет шаблон для бесплатной функции, которая вызывает функцию-член. Затем, если ваш объект предоставляет функцию с тем же именем с помощью ADL, это будет лучше, чем создание экземпляра шаблона, и, следовательно, он будет выбран первым. Как говорит Space_C0wb0y, они могут использовать SFINAE для обнаружения функции-члена в шаблоне и делать что-то другое в зависимости от того, существует она или нет.

Вы не можете изменить поведение std::cout << x;, добавив функцию-член в x, поэтому я не совсем понимаю, что вы имеете в виду.

person Steve Jessop    schedule 14.03.2011
comment
Другим механизмом, важным для диспетчеризации, является SFINAE. - person Björn Pollex; 14.03.2011
comment
спасибо за подсказки, это должно помочь мне разобраться. ТАКЖЕ, вы были правы насчет семантики потока, и я удалил этот пример. Последний такой код, на который я смотрел, был boost::serialization, который позволяет вам предоставлять оба типа функций. - person Hassan Syed; 14.03.2011

Если вы просто ищете конкретный пример, рассмотрите следующее:

#include <cassert>
#include <type_traits>
#include <iostream>

namespace NS
{
    enum direction { forward, backward, left, right };

    struct vehicle { virtual ~vehicle() { } };

    struct Car : vehicle
    {
        void MoveForward(int units) // (1)
        {
            std::cout << "in NS::Car::MoveForward(int)\n";
        }
    };

    void MoveForward(Car& car_, int units)
    {
        std::cout << "in NS::MoveForward(Car&, int)\n";
    }
}

template<typename V>
class HasMoveForwardMember // (2)
{
    template<typename U, void(U::*)(int) = &U::MoveForward>
    struct sfinae_impl { };

    typedef char true_t;
    struct false_t { true_t f[2]; };

    static V* make();

    template<typename U>
    static true_t check(U*, sfinae_impl<U>* = 0);
    static false_t check(...);

public:
    static bool const value = sizeof(check(make())) == sizeof(true_t);
};

template<typename V, bool HasMember = HasMoveForwardMember<V>::value>
struct MoveForwardDispatcher // (3)
{
    static void MoveForward(V& v_, int units) { v_.MoveForward(units); }
};

template<typename V>
struct MoveForwardDispatcher<V, false> // (3)
{
    static void MoveForward(V& v_, int units) { NS::MoveForward(v_, units); }
};

template<typename V>
typename std::enable_if<std::is_base_of<NS::vehicle, V>::value>::type // (4)
mover(NS::direction d, V& v_)
{
    switch (d)
    {
    case NS::forward:
        MoveForwardDispatcher<V>::MoveForward(v_, 1); // (5)
        break;
    case NS::backward:
        // ...
        break;
    case NS::left:
        // ...
        break;
    case NS::right:
        // ...
        break;
    default:
        assert(false);
    }
}

struct NonVehicleWithMoveForward { void MoveForward(int) { } }; // (6)

int main()
{
    NS::Car v; // (7)
    //NonVehicleWithMoveForward v;  // (8)
    mover(NS::forward, v);
}

HasMoveForwardMember (2) — это метафункция, проверяющая наличие функции-члена с таким именем и сигнатурой void(V::*)(int) в заданном классе V. MoveForwardDispatcher (3) использует эту информацию для вызова функции-члена, если она существует, или возвращается к вызову свободной функции, если она не существует. mover просто делегирует вызов MoveForward MoveForwardDispatcher (5).

Опубликованный код вызовет Car::MoveForward (1), но если эта функция-член будет удалена, переименована или изменена ее подпись, вместо этого будет вызвана NS::MoveForward.

Также обратите внимание, что, поскольку mover является шаблоном, необходимо выполнить проверку SFINAE, чтобы сохранить семантику, позволяющую передавать только объекты, производные от NS::vehicle, для v_ (4). Чтобы продемонстрировать, если закомментировать (7) и раскомментировать (8), mover будет вызываться с объектом типа NonVehicleWithMoveForward (6). , который мы хотим запретить, несмотря на то, что HasMoveForwardMember<NonVehicleWithMoveForward>::value == true.

(Примечание: если ваша стандартная библиотека не поставляется с std::enable_if и std::is_base_of, используйте доступные варианты std::tr1:: или boost::.)

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

person ildjarn    schedule 29.03.2011

Хотя иногда разработчики могут использовать бесплатные функции или функции класса взаимозаменяемо, в некоторых ситуациях они могут использовать друг друга.

(1) Функции объекта/класса («методы») предпочтительнее, когда большая часть их целей затрагивает только объект или объекты предназначены для составления других объектов.

// object method
MyListObject.add(MyItemObject);
MyListObject.add(MyItemObject);
MyListObject.add(MyItemObject);

(2) Предпочтение отдается свободным ("глобальным" или "модульным") функциям, когда речь идет о нескольких объектах, и объекты не являются частью/состоят друг из друга. Или, когда функция использует простые данные (структуры без методов, примитивные типы).

MyStringNamespace.MyStringClass A = new MyStringNamespace.MyStringClass("Mercury");
MyStringNamespace.MyStringClass B = new MyStringNamespace.MyStringClass("Jupiter"); 
// free function
bool X = MyStringNamespace.AreEqual(A, B);

Когда некоторые общие функции модуля обращаются к объектам, в С++ у вас есть «ключевое слово друга», которое позволяет им получать доступ к методам объектов, не учитывая область.

class MyStringClass {
  private:
    // ...
  protected:
    // ...
  // not a method, but declared, to allow access
  friend:
    bool AreEqual(MyStringClass A, MyStringClass B);
}

bool AreEqual(MyStringClass A, MyStringClass B) { ... }

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

person umlcat    schedule 24.03.2011

Если я правильно понял, ваша проблема просто решается с помощью (возможно, множественного) наследования. У вас есть где-то свободная функция пространства имен:

namespace NS {
void DoSomething()
{
    std::cout << "NS::DoSomething()" << std::endl;
}
} // namespace NS

Используйте базовый класс, который перенаправляет ту же функцию:

struct SomethingBase
{
    void DoSomething()
    {
        return NS::DoSomething();
    }
};

Если некоторый класс A, производный от SomethingBase, не реализует вызов DoSomething(), он вызовет SomethingBase::DoSomething() -> NS::DoSomething():

struct A : public SomethingBase // probably other bases
{
    void DoSomethingElse()
    {
        std::cout << "A::DoSomethingElse()" << std::endl;
    }
};

Если другой класс B, производный от SomethingBase, реализует вызов DoSomething(), он вызовет B::DoSomething():

struct B : public SomethingBase // probably other bases

{
    void DoSomething()
    {
        std::cout << "B::DoSomething()" << std::endl;
    }
};

Таким образом, вызов DoSomething() для объекта, производного от SomethingBase, выполнит член, если он существует, или свободную функцию в противном случае. Обратите внимание, что нечего бросать, вы получите ошибку компиляции, если нет совпадения с вашим вызовом.

int main()
{
    A a;
    B b;
    a.DoSomething(); // "NS::DoSomething()"
    b.DoSomething(); // "B::DoSomething()"
    a.DoSomethingElse(); // "A::DoSomethingElse()"
    b.DoSomethingElse(); // error 'DoSomethingElse' : is not a member of 'B'
}
person Alain Rist    schedule 30.03.2011