Kane
Kane

Reputation: 6270

static_assert before a constructor's initialiser list

There is a non-templated class which has a templated constructor. Is it possible to check a static assertion before initialising member variables in such constructor?

For example, the following code executes T::value() before checking that T has such method.

class MyClass
{
public:
    template<typename T>
    MyClass(const T &t)
        : m_value(t.value())
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
    }

private:
    int m_value;
};

Placing static_assert in a constructor's body works fine except it prints "T must have a value() method" at the very end, after all error messages from the member initialiser list, e.g.:

prog.cpp: In instantiation of ‘MyClass::MyClass(const T&) [with T = int]’:
prog.cpp:24:16:   required from here
prog.cpp:12:21: error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
         : m_value(t.value())
                   ~~^~~~~
prog.cpp:14:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");
         ^~~~~~~~~~~~~

I find this a bit confusing and wonder if it would be possible to print "T must have a value() method" before trying to initialise member variables.

I know that I could use enable_if and SFINAE to disable this constructor for inappropriate Ts, but I would like to tell a user something more meaningful than "method not found".

Upvotes: 11

Views: 4061

Answers (3)

M&#225;rio Feroldi
M&#225;rio Feroldi

Reputation: 3591

You can use std::enable_if to SFINAE out the constructor that does the static_assert based on whether T has the function member value(), keeping the real implementation separated.

The first constructor is selected if T has the value() method, and is implemented as normally (except that it needs the std::enable_if in order to be selected):

template <typename T, typename = std::enable_if_t<HasValueMethod<T>::value>>
MyClass(const T &t) : m_value(t.value())
{}

So we need the second constructor to be SFINAEd out of function overloading, since the first one already knows that T::value exists:

template <typename T, typename = std::enable_if_t<!HasValueMethod<T>::value>>
MyClass(const T &, ...)
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
}

Note the variadic parameter ...: it is needed in order to differentiate the constructor's prototype, so it doesn't collide with the first one (they need to be different, otherwise ambiguous prototypes result in compile error). You won't pass anything to it, it's just there to make it a different prototype.

Note as well that the predicate for std::enable_if is the same but negated. When HasValueMethod<T>::value is false, the first constructor is SFINAEd out of function overloading, but not the second one, which then would trigger the static assert.

You still need to use HasValueMethod<T>::value in the static assert's parameter, so it depends on T to be executed. Otherwise, putting just false there would make it always trigger regardless of being selected out.

Here's what GCC prints when T has no .value():

main.cpp: In instantiation of 'MyClass::MyClass(const T&, ...) [with T = A; <template-parameter-1-2> = void]':
main.cpp:35:18:   required from here
main.cpp:21:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");

         ^~~~~~~~~~~~~

Here's Clang's:

main.cpp:21:9: error: static_assert failed "T must have a value() method"
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        ^    

All in all, there's an issue (as pointed out by @T.C. in comments) with this approach: MyClass is now convertible from anything from the point of view of unevaluated contexts. That is,

static_assert(std::is_convertible_v</*anything*/, MyClass>); // Always true.

In C++20, when hopefully concepts are in, this is easily solved with a requires clause:

template <typename T>
  requires HasValueMethod<T>::value
MyClass(const T &t) : m_value(t.value())
{}

You could directly express HasValueMethod<T> in the requires clause just as well:

template <typename T>
  requires requires (T a) { { a.value() } -> int; }
MyClass(const T &t) : m_value(t.value())
{}

Or transforming HasValueMethod<T> into a real concept:

template <typename T>
concept HasValueMethod = requires (T a) {
    { a.value() } -> int;
};

// Inside `class MyClass`.
template <typename T>
  requires HasValueMethod<T>
MyClass(const T &t) : m_value(t.value())
{}

Such solutions make std::is_convertible_v<T, MyClass> work as expected as well.

Upvotes: 6

Ben Voigt
Ben Voigt

Reputation: 283773

Bring the static_assert() closer to the usage. In this case, a helper function will do it:

class MyClass
{
    template<typename T>
    static int get_value(const T& t)
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        return t.value();
    }

public:
    template<typename T>
    MyClass(const T &t)
        : m_value(get_value(t))
    {
    }

private:
    int m_value;
};

Not only does this fix the order of the error messages, but it lets you reuse the message for every path that needs the value() member function.

Upvotes: 4

Massimiliano Janes
Massimiliano Janes

Reputation: 5624

if you don't plan to SFINAE-constrain the constructor and you always want an error to be raised when HasValueMethod is false, you could just write an 'hard' variant of your trait class:

template<class T>
struct AssertValueMethod
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
  using type = void; // note: needed to ensure instantiation, see below ...
};

template< typename T, typename = typename AssertValueMethod<T>::type >
MyClass(const T &t): ...

moreover, if, later on, you want to add a sfinae selected overload, you can always write a proper delegating constructor without changing the static assertion logic ...

Upvotes: 4

Related Questions