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".
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With