Enable a non-template member function iff it would typecheck

140 Views Asked by At

For example

template <class T1, class T2>
class foo
{
    T1 t1;
    T2 t2;

    T1 bar(); //Always exists
    decltype(t1(t2)) baz(); //Should only exist if t1(t2) is valid
};

If baz is invalid I still want the program to compile as long as nobody actually calls baz.

2

There are 2 best solutions below

0
On BEST ANSWER

You can make baz a template, such that if the return type is invalid the member will be SFINAE-d out rather than result in a hard error:

template <class T1, class T2>
class foo
{
    T1 t1;
    T2 t2;

    T1 bar(); //Always exists

    template<typename T = T1>
    decltype(std::declval<T&>()(t2)) baz();
};

The T parameter is necessary to make the computed expression type-dependent, or else SFINAE doesn't apply. If you're worried that this implementation detail 'leaks' out and that someone might attempt f.baz<int>(), you can declare baz with template<typename... Dummy, typename T = T1> and enforce proper usage with e.g. static_assert( sizeof...(Dummy) == 0, "Incorrect usage" ); in the function body. Both approaches do make it harder to take the address of the member: it should look like e.g. &foo<T, U>::baz<>.

Another approach is to introduce a class template specialization:

template<typename...> struct void_ { using type = void; };

template<typename T1, typename T2, typename = void>
class foo {
    // version without baz
};

template<typename T1, typename T2>
class foo<T1, T2, typename void_<decltype(std::declval<T1&>()(std::declval<T2>()))>::type> {
    decltype(std::declval<T1&>()(std::declval<T2>())) baz();
};

In this case &foo<T, U>::baz is fine for taking the address of the member (assuming it is present of course). Code that is common to both specialization can be factored out in a common base, and if there is a worry that the additional template parameter that is introduced as an implementation detail might leak it is possible to have a 'real' foo taking only two template parameters in turn inheriting from such an implementation.

0
On

While this question is for C++11, I think it's worth providing the modern C++20 approach as well, using constraints:

#include <concepts>

template <class T1, class T2>
class foo
{
    T1 t1;
    T2 t2;

    T1 bar();
    auto baz() requires std::invocable<T1, T2>;
};

baz() has a deduced return type so that you don't get a compiler error due to decltype(t1(t2)) being invalid. As a result, it has to be defined prior to its use, but since foo is a class template, that's usually the case anyway.

baz() is also given a trailing requires-clause, which "disables" the member if the condition is false. std::invocable isn't exactly the same as checking whether t1(t2) is valid, but if you need exactly that check, you can also write your own concept:

template <typename T, typename... Args>
concept callable_with = requires (T& t, Args&... args) {
    // TODO: maybe use perfect forwarding here?
    //       once again, that would not be *exactly* t1(t2) anymore
    t(args...);
};

And then:

auto baz() requires callable_with<T1, T2>;