Search code examples
c++templatesvtable

Virtual Functions and Generic Programming


Suppose I have a template class as such:

template <class T>
class Foo
{
public:

    explicit Foo(const T& value)
        : m_Value(value)
    {
    }

    bool Bar(const T& value)
    {
        return m_Value == value;
    }

private:

    T m_Value;
};

And let's say I have some other user type such as:

class A
{
};

Then this code is perfectly valid, even though class A does not define the equality operator:

int main(int argc, char* argv[])
{
    A a;
    Foo<A> foo(a);

    return 0;
}

But if I make Foo::Bar() virtual:

    virtual bool Bar(const T& value)
    {
        return m_Value == value;
    }

The code no longer compiles:

error C2676: binary '==': 'A' does not define this operator or a conversion to a type acceptable to the predefined operator

I fully understand why this is a problem. Correct me if I'm wrong, but my understanding is that because the function is virtual, the compiler must compile the function (even if it is never called) so that it can reference it in the v-table of Foo.

I'm wondering if there is a way around this problem. I want to have a template class that deals with generic types that may only implement partial interfaces. As long as the missing bits are not used, the code should compile fine. This is similar to how a lot of STD containers work already, but they don't use virtual functions.

How do I do this? Is there an elegant solution to this?


Solution

  • As explained by Kerrek SB above, virtual functions are always instanciated when the template is instanciated. So there is no way to have your program compile just fine when the virtual method is not used and have it fail to compile in case it is used and the class you want to wrap does not supply its own operator==.

    But you can, however, make the program crash at runtime (with assert/terminate) or throw an exception.

    Disclaimer: I don't think it is a good idea to do what you are trying to do, as it allows to create classes that don't support the interface they claim to provide. Use the following at your own risk.

    The way to go here is to use custom type traits for every method you want to supply even though the wrapped class does not implement it itself. In your case above this is only operator==, and the corresponding code looks something like this:

    namespace traits {
    template <typename T>
    using operator_eq_t = decltype(std::declval<T>() == std::declval<T>());
    
    template <typename, typename = void>
    struct has_operator_eq : std::false_type {};
    
    // check that operator== is defined and returns the correct type `bool`.
    template <typename T>
    struct has_operator_eq<T, std::void_t<operator_eq_t<T>>> 
        : std::is_same<operator_eq_t<T>, bool> {}; 
    } // namespace traits
    

    If you don't have access to c++1z yet you can make your own version of std::void_t, everything else is valid C++14:

    template <typename...>
    using void_t = void
    

    With that in place you can create your wrapper class template with tag dispatch:

    template <typename T>
    class Foo : public IFoo<T> {
    public:
        explicit Foo(T const& value)
            : m_Value(value) {
        }
    
        bool Bar(T const& value) override {
            return BarImpl(value, traits::has_operator_eq<T>{});
        }
    
    private:
        T m_Value;
    
        bool BarImpl(T const& value, std::false_type) {
            // some sensible default, in this case you might
            // consider just to return false
            assert(!"Called `Bar` on class that does not implement `operator==`.");
            throw std::logic_error("Called `Bar` on class that does not implement `operator==`.");
        }
    
        bool BarImpl(T const& value, std::true_type) {
            return value == m_Value;
        }
    };
    

    A working example can be found here.