0

In a templated class, can we check a member function's signature and define different compilation behaviors for different subclasses? To be more specific, consider the following simple example:

template <typename T>
class Foo {
    // ingore all other functions

    virtual std::shared_ptr<T> do_something(int a) {
        return std::make_shared<T>(a);
    }
};

This should just work fine with the T1 class:

class T1 {
    T1(int a) {
        // implememntation
    }

    // ingore all other functions
};

// Foo<T1> would just work

However, it will fail the compilation with T2 because Foo's do_something function is clearly implemented to call T's constructor with one argument only:

class T2 {
    T2(int a, int b) {
        // implememntation
    }

    // ingore all other functions
};

// Foo<T2> would fail to compile

So the question is, can we rework on Foo to let it work for both T1 and T2, in the way that, for T1 and classes whose constructor takes one int argument, it will compile with a default implementation, whereas for T2 and classes whose constructors are different, it will compile with a virtual function and enforce subclass to implement it with override. Somewhat like below:

Template <typename T>
class Foo {
    // ingore all other functions

    /* if T's signature takes one int input only
     * compile like the below
     */
    virtual std::shared_ptr<T> do_something(int x) {
        return std::make_shared<T>(x);
    }

    /* if T's signature is different
     * compile like the below and let the subclass implement it
     */
    virtual std::shared_ptr<T> do_something(int x) = 0;

}

Is this possible without using any 3rd-party library? It's acceptable if we have to use macros/preprocessors.

It is also acceptable if there must be one function do_something there but for the case of mismatched signature, it just raises runtime exception, something like:

Template <typename T>
class Foo {
    // ingore all other functions

    virtual std::shared_ptr<T> do_something(int x) {
        /* if T's signature takes one int input only
         * compile like the below
         */
        // return std::make_shared<T>(x);
        /* if T's signature is different
         * compile like the below and let the subclass implement it
         */
        // std::throws...
    }
    
}
Bruce
  • 1,608
  • 2
  • 17
  • 29
  • Is there a specific reason for `do_something` to be virtual? – Sam Varshavchik Jul 07 '21 at 00:43
  • Yes, because the subclasses may want to override it in the application; and other functions of `Foo` may need to call the overriden `do_something` too. – Bruce Jul 07 '21 at 00:45
  • It is not possible in C++ to have two class methods with the same signature, and SFINAE won't help. What would be possible is for `Foo`, itself, to inherit from a specialized class based on `T` whose specialization will implement the appropriate `do_something` alternative. This would be a pile of code, but it's possible, and the same thing needs to be done for each `do_something` overload. That's the most that can be done here. – Sam Varshavchik Jul 07 '21 at 00:55
  • Why not simply make `Foo::do_something()` be a variadic template so it can take any number of constructor parameters? – Remy Lebeau Jul 07 '21 at 00:55
  • Because it has to be virtual, see above. – Sam Varshavchik Jul 07 '21 at 00:56
  • @SamVarshavchik in that case, can't `do_something()` be overloaded, or use `if constexpr`, using SFINAE based on `std::is_invocable`? – Remy Lebeau Jul 07 '21 at 00:59
  • @SamVarshavchik Thanks for the insights. So, is it still impossible by using preprocessors to guide it so there is always "one" function with same signature there, but just under different #if branches? – Bruce Jul 07 '21 at 01:00
  • Well, if it's not invocable, the stated intention is for the function to be abstract. Perhaps a runtime exception would be a compromise. – Sam Varshavchik Jul 07 '21 at 01:00
  • Preprocessor stuff happens way, way before things like function signatures are examined. By the time the C++ compiler figures out what the various signatures are, preprocessing is just a faint memory. – Sam Varshavchik Jul 07 '21 at 01:01
  • @RemyLebeau and Sam, throwing a runtime exception is also acceptable if must not be a pure virtual function without any implementation. Could you elaborate the idea there? – Bruce Jul 07 '21 at 01:04
  • @SamVarshavchik Thanks. I've updated the question to allow throwing runtime exception instead of pure virtual function. – Bruce Jul 07 '21 at 01:07
  • Then an `if constexpr`-based approach should work, as was mentioned earlier. That would be the simplest solution, for C++17. Before C++17 it would be a lot more work. – Sam Varshavchik Jul 07 '21 at 01:15
  • @SamVarshavchik Sounds good. Could you elaborate it? I think that can be the answer. Thanks a bunch. – Bruce Jul 07 '21 at 01:34
  • We're talking about some fairly advanced C++ techniques. Instead of cobbling together random code fragments, it will be far more meaningful to invest some quality time [on a good textbook on advanced C++17](https://stackoverflow.com/questions/388242/) and get the full picture, not to mention useful information on all the other modern C++ features. – Sam Varshavchik Jul 07 '21 at 01:39

1 Answers1

2

As far as I can tell, we need class template specialization here. Not even C++20 requires-clauses can be applied to virtual functions, so the only thing we can do is have the whole class change.

template<typename T> // using C++20 right now to avoid SFINAE madness
struct Foo {
    virtual ~Foo() = default;
    virtual std::shared_ptr<T> do_something(int a) = 0;
};
template<std::constructible_from<int> T>
struct Foo<T> {
    virtual ~Foo() = default; // to demonstrate the issue of having to duplicate the rest of the class
    virtual std::shared_ptr<T> do_something(int a) {
       return std::make_shared<T>(a);
    }
};

If there is a lot of stuff in Foo, you can avoid duplicating it with a heap of upfront cost by moving do_something to its own class.

namespace detail { // this class should not be touched by users
    template<typename T>
    struct FooDoSomething {
        virtual ~FooDoSomething() = default;
        virtual std::shared_ptr<T> do_something(int a) = 0;
    };
    template<std::constructible_from<int> T>
    struct FooDoSomething<T> {
        virtual ~FooDoSomething() = default;
        virtual std::shared_ptr<T> do_something(int a);
    };
}
template<typename T>
struct Foo : detail::FooDoSomething<T> {
    // other stuff, not duplicated
    // just an example
    virtual int foo(int a) = 0;
};
namespace detail {
    template<std::constructible_from<int> T>
    std::shared_ptr<T> FooDoSomething<T>::do_something(int a) {
        Foo<T> &thiz = *static_cast<Foo<T>*>(this); // if you need "Foo things" in this default implementation, then FooDoSomething is *definitely* unsafe to expose to users!
        return std::make_shared<T>(thiz.foo(a));
    }
}

Godbolt

To convert this down from C++20, replace the concept-based specialization with old-type branching:

// e.g. for the simple solution
template<typename T, bool = std::is_constructible_v<T, int>>
struct Foo { // false case
    // etc.
};
template<typename T>
struct Foo<T, true> { // true case
    // etc.
};
// or do this to FooDoSomething if you choose to use that

A runtime error is much easier, at least in C++17 and up, since you can just use a if constexpr to avoid compiling the problematic code

template<typename T>
struct Foo {
    virtual ~Foo() = default;
    virtual std::shared_ptr<T> do_something(int a) {
        if constexpr(std::is_constructible_v<T, int>) return std::make_shared<T>(a);
        else throw std::logic_error("unimplemented Foo<T>::do_something");
    }
};
HTNW
  • 27,182
  • 1
  • 32
  • 60