Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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".

like image 702
Kane Avatar asked Nov 26 '17 13:11

Kane


Video Answer


1 Answers

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.

like image 68
Mário Feroldi Avatar answered Sep 19 '22 04:09

Mário Feroldi