Reputation: 6270
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 T
s, but I would like to tell a user something more meaningful than "method not found".
Upvotes: 11
Views: 4061
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
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
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