Механизм расширения через свободные функции или функции-члены

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

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

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"

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

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

!! ДЛЯ WIN BOUNTY!!

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

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

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

Ответ 1

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

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

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

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

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

Пример:

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

EDIT: после прочтения ответа Стива Джессопа, я понимаю, что в основном то, что он написал, только с большим количеством слов:)

Ответ 2

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

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

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

Ответ 3

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

#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) и uncomments (8), mover будет вызываться с объектом типа NonVehicleWithMoveForward (6), который мы хотим запретить, несмотря на то, что HasMoveForwardMember<NonVehicleWithMoveForward>::value == true.

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

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

Ответ 4

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

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

Ответ 5

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

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'
}