The mechanics of extension via free functions or member functions

后端 未结 5 936
挽巷
挽巷 2021-02-14 12:45

Loads of C++ libraries, the standard included, allow you to adapt your objects for use in the libraries. The choice is often between a member function or a free function in the

相关标签:
5条回答
  • 2021-02-14 12:52

    If you're just looking for a concrete example, consider the following:

    #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) is a metafunction that checks for the existence of a member function of that name with the signature void(V::*)(int) in a given class V. MoveForwardDispatcher (3) uses this information to call the member function if it exists or falls back to calling a free function if it doesn't. mover simply delegates the invocation of MoveForward to MoveForwardDispatcher (5).

    The code as-posted will invoke Car::MoveForward (1), but if this member function is removed, renamed, or has its signature changed, NS::MoveForward will be called instead.

    Also note that because mover is a template, a SFINAE check must be put in place to retain the semantics of only allowing objects derived from NS::vehicle to be passed in for v_ (4). To demonstrate, if one comments out (7) and uncomments (8), mover will be called with an object of type NonVehicleWithMoveForward (6), which we want to disallow despite the fact that HasMoveForwardMember<NonVehicleWithMoveForward>::value == true.

    (Note: If your standard library does not come with std::enable_if and std::is_base_of, use the std::tr1:: or boost:: variants instead as available.)

    The way this sort of code is usually used is to always call the free function, and implement the free function in terms of something like MoveForwardDispatcher such that the free function simply calls the passed in object's member function if it exists, without having to write overloads of that free function for every possible type that may have an appropriate member function.

    0 讨论(0)
  • 2021-02-14 12:55

    If I understood correctly your problem is simply solved using (maybe multiple) inheritance. You have somewhere a namespace free function:

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

    Use a base class which forwards the same function:

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

    If some class A deriving from SomethingBase does not implement DoSomething() calling it will call SomethingBase::DoSomething() -> NS::DoSomething():

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

    If another class B deriving from SomethingBase implement DoSomething() calling it will call B::DoSomething():

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

    So calling DoSomething() on an object deriving from SomethingBase will execute the member if existing, or the free function otherwise. Note that there is nothing to throw, you get a compile error if there is no match to your call.

    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 讨论(0)
  • 2021-02-14 12:58

    Well, I can tell you how to detect the presence of member functions of a certain name (and signature) at compile time. A friend of mine describes it here:

    Detecting the Existence of Member Functions at Compile-Time

    However that won't get you where you want to go, because it only works for the static type. Since you want to pass a "reference-to-vehicle", there is no way to test if the the dynamic type (the type of the concrete object behind the reference) has such a member function.

    If you settle for the static type though, there is another way to do a very similar thing. It implements "if the user provides an overloaded free function, call it, otherwise try to call the member function". And it goes like this:

    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
    

    That way the user can provide it's own overload of "the_operation", in the same namespace as his class, so it's found by ADL. Of course the user's "the_operation" must be "more specialized" than your default implementation - otherwise the call would be ambiguous. In practice that's not a problem though, since everything that restricts the type of the parameter more than it being a reference-to-const to anything will be "more specialized".

    Example:

    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: after reading Steve Jessop's answer again, I realize that's basically what he wrote, only with more words :)

    0 讨论(0)
  • 2021-02-14 13:06

    The library doesn't do any of this at runtime, dispatch is done by the compiler when the calling code is compiled. Free functions in the same namespace as one of the arguments are found according to the rules of a mechanism called "Argument-Dependent Lookup" (ADL), sometimes called "Koenig lookup".

    In cases where you have the option either to implement a free function or a member function, it may be because the library provides a template for a free function that calls the member function. Then if your object provides a function of the same name by ADL, it will be a better match than instantiating the template, and hence will be chosen first. As Space_C0wb0y says, they might use SFINAE to detect the member function in the template, and do something different according to whether it exists or not.

    You can't change the behaviour of std::cout << x; by adding a member function to x, so I'm not quite sure what you mean there.

    0 讨论(0)
  • 2021-02-14 13:12

    Altought, sometimes, developers can used free functions or class functions, interchangeably, there are some situations, to use one another.

    (1) Object / Class functions ("methods), are prefered when most of its purpouse affect only the object, or objects are inteded to compose other objects.

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

    (2) Free ("global" or "module") functions are prefered, when involves several objects, and the objects are not part / composed of each other. Or, when the function uses plain data (structs without methods, primitive types).

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

    When some common module function access objects, in C++, you have the "friend keyword" that allow them to access the objects methods, without regarding scope.

    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) { ... }
    

    In "almost pure object oriented" programming languages like Java or C#, where you can't have free functions, free functions are replaced with static methods, which makes stuff more complicated.

    0 讨论(0)
提交回复
热议问题