C++ interface without inheritance for template functions

Viewed 464

There are two different classes with intersecting set of methods:

class A {
public:
    int Value1() { return 100; }
    char Value2() { return "a"; }
    void actionA() { };
}

class B {
public:
    int Value1() { return 200; }
    char Value2() { return "b"; }
    void actionB() { };
}

The common part of the classes interface can be described like this:

class GenericPart {
public:
  virtual int Value1();
  virtual char Value2();
}

Please note that classes A and B come from some library and therefore cannot be inherited from GenericPart.

There is a template function that works with objects implementing methods described in GenericPart:

template <typename T>
void function(T record) {
    std::cout << record.Value1() << " " << record.Value2() << std::endl;
}

Is it possible to specialize this template function to make it receive only objects that match GenericPart?

4 Answers

You could use the C++20 feature: concepts & constraints, but a simpler solution may involve static_assert. See the comments in function for some explanation

#include <type_traits>
#include <iostream>

class A {
public:
    int Value1() { return 100; }
    char Value2() { return 'a'; }
    void actionA() { };
};

class B {
public:
    int Value1() { return 200; }
    char Value2() { return 'b'; }
    void actionB() { };
};

class C  { // Has the required functions but with different return types
public:
    double Value1() { return 0.0; }
    double Value2() { return 1.0; }
};

class D  { // Has only one required function
public:
    int Value1() { return 300; }
};


template <typename T>
void function(T record) {
    // Check statically that T contains a `Value1()` that returns an int
    static_assert(std::is_same_v<int, decltype(record.Value1())>, "int Value1() required!");

    // Check statically that T contains a `Value2()` that returns an char
    static_assert(std::is_same_v<char, decltype(record.Value2())>, "char Value2() required!");

    std::cout << record.Value1() << " " << record.Value2() << std::endl;
}

int main()
{
    A a;
    B b;
    C c;
    D d;

    function(a); // Ok
    function(b); // Ok
    function(c); // causes static_assert to fail
    function(d); // causes static_assert to fail
}

Is it possible to specialize this template function to make it receive only objects that match GenericPart?

You can't partially specialize a template function.

But you can SFINAE enable/disable different version of it, according to the characteristicsf of T.

By example, you can enable function() only for T supporting Value1() and Value2() methods as follows

template <typename T>
auto function (T record)
   -> decltype( record.Value1(), record.Value2(), void() )
 { std::cout << record.Value1() << " " << record.Value2() << std::endl; }

It's more complicated disable a function() if doesn't support some methods (but see Timo's answer: given a has_feature_set become trivial) so I propose to add an unused parameter of some type (by example, int)

template <typename T> // VVV  <-- unused argument
auto function (T record, int)
   -> decltype( record.Value1(), record.Value2(), void() )
 { std::cout << record.Value1() << " " << record.Value2() << std::endl; }

and develop a generic function that accept an unused type of another (but compatible) type (by example, long) and is ever enabled

template <typename T> // VVVV  <-- unused argument
void function (T record, long)
 { /* do something with record */ }

Now you can write a first level function() as follows

template <typename T>
void function (T record)
 { function(record, 0); }

Observe that you pass 0 (a int) to the second level function().

This way is executed the exact match, function(T, int), if available (that is: if T support the requested methods). Otherwise execute the generic version, function(T, long), that is ever available.

Similar to max66's approach but with return type checking:

namespace detail
{
    template <typename T>
    std::false_type has_feature_set_helper(...); // fallback if anything goes wrong in the helper function below

    template <typename T>
    auto has_feature_set_helper(int) -> std::conjunction<
        std::is_invocable_r<int, decltype(&T::Value1), T>, // assert that Value1 is a member function that returns something that is convertible to int
        std::is_invocable_r<char, decltype(&T::Value2), T> // assert that Value2 is a member function that returns something that is convertible to char
    >;
}

template <typename T>
constexpr bool has_feature_set = decltype(detail::has_feature_set_helper<T>(0))::value;

template <typename T>
auto function(T record) -> std::enable_if_t<has_feature_set<T>>
{
    std::cout << record.Value1() << " " << record.Value2() << std::endl;
}

Here is a full example.

Note that the return type checking is not strict, meaning it's checked if the return type is convertible to the type specified in is_invocable_r_v.

You could design an intermediate template class derived from GenericPart. This template you then specialize for A and B.

template<class T>
class GenericPartTemplate : public GenericPart
{
public:
  int Value1() override;
  char Value2() override;
}

Next it is no longer needed to make your function a template:

void function(GenericPart& record) {
    std::cout << record.Value1() << " " << record.Value2() << std::endl;
}

If you want to enforce only using A and B you can use type_traits to check in compile time GenericPartTemplate is only used with those types.

Related