Механика расширения с помощью свободных функций или функций-членов - PullRequest
10 голосов
/ 14 марта 2011

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

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

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.

Ответы [ 5 ]

8 голосов
/ 14 марта 2011

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

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

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

2 голосов
/ 24 марта 2011

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

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

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

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

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

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

1 голос
/ 30 марта 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, так что свободная функция просто вызывает переданную функцию-член объекта, если она существует, без необходимости переписывать эту свободную функцию для каждого возможного типа, который может иметь соответствующую функцию-член.

0 голосов
/ 30 марта 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'
}
0 голосов
/ 24 марта 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);

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

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

...