How can you enable a class template member function only if a template argument was provided?

200 Views Asked by At

Is it possible to have a class with an optional template parameter that can be called like this?:

#include <iostream>

template <typename T = void>
class A final
{
public:
    // This class can be called only when T exists.
    void f()
    {
        printf("%d\n", member);
    }

    // This method can be called only when T is missing.
    void g()
    {
        printf("No template parameter\n");
    }
public:
    T member;
};

int main()
{

    A<int> a1;
    A a2;
    a1.f(); // should be valid
    a1.g(); // should be invalid, cannot compile
    a2.f(); // should be invalid, cannot compile
    a2.g(); // should be valid

    return 0;
}

If yes, what are the std functions that should be used?

2

There are 2 best solutions below

1
Jarod42 On BEST ANSWER

You might use the "old" way with specialization:

template <typename T = void>
class A final // generic case, T != void
{
public:
    void f() { std::cout << member << std::endl; }

public:
    T member;
};

template <>
class A<void> final
{
public:
    void g() { printf("No template parameter\n"); } // actually `void` and not "No".
};

By turning your class into variadic, to handle absent parameter as empty pack, instead of void, you might do:

template <typename... Ts>
class A final
{
static_assert(sizeof...(Ts) < 2);
public:
    void f() requires (sizeof...(Ts) == 1) { std::cout << std::get<0>(member) << std::endl; }
    void g() requires (sizeof...(Ts) == 0) { printf("No template parameter\n"); }

public:
    std::tuple<Ts...> member;
};
6
dfrib On

Constraints of the member function API

You can leverage constraints and concepts, particularly the std::same_as concept:

#include <concepts>

template <typename T = void>
struct A final {
    // This method is defined only when T is non-void.
    void f() requires (!std::same_as<T, void>) {} 

    // This method is defined only when T is void.
    void g() requires std::same_as<T, void> {} 
};

int main() {
    A<int> a1;
    A a2;
    a1.f(); // OK
    // a1.g(); // error
    // a2.f(); // error
    a2.g(); // OK
}

What about the member?

If you'd only like the member to exist for certain specializations, you could wrap it in a class template which you specialize accordingly. E.g.:

template<typename T>
struct Value {
    T value;
};

template<>
struct Value<void> {};

Another approach would be to use a helper base class to inherit the member(s), which can be helpful particularly if the constructors of A are dependent on traits on the type itself. E.g. std::optional is typically implemented using mixins of various base classes.

You may also want to place restrictions on the A class' template parameter itself w.r.t. what kind of types that makes sense to wrap in the class.

Using the minimal approach:

#include <concepts>

template<typename T>
struct Value {
    T value;
};

template<>
struct Value<void> {};

template <typename T = void>
struct A final {
    // This methods can be called only when T is non-void.
    T f() requires (!std::same_as<T, void>) {
        return member.value;
    } 

    // This method can be called only when T is void.
    void g() requires std::same_as<T, void> {}
private:
    Value<T> member;
};

int main() {
    A<int> a1;
    A a2;
    auto value = a1.f(); // OK
    // a1.g(); // error
    // a2.f(); // error
    a2.g(); // OK
}