The standard library utility declval
is defined as:
template<class T> add_rvalue_reference_t<T> declval() noexcept;
To add a rvalue reference here seemed like a good idea, if you think about the language when it was introduced in C++11: Returning a value involved a temporary, that was subsequently moved from. Now C++17 introduced guaranteed copy elision and this does not apply any more. As cppref puts it:
C++17 core language specification of prvalues and temporaries is fundamentally different from that of the earlier C++ revisions: there is no longer a temporary to copy/move from. Another way to describe C++17 mechanics is "unmaterialized value passing": prvalues are returned and used without ever materializing a temporary.
This has some consequences on other utilities implemented in terms of declval
. Have a look at this example (view on godbolt.org):
#include <type_traits>
struct Class {
explicit Class() noexcept {}
Class& operator=(Class&&) noexcept = delete;
};
Class getClass() {
return Class();
}
void test() noexcept {
Class c{getClass()}; // succeeds in C++17 because of guaranteed copy elision
}
static_assert(std::is_constructible<Class, Class>::value); // fails because move ctor is deleted
Here we have a nonmovable class. Because of guaranteed copy elision, it can be returned from a function and then locally materialised in test()
. However the is_construtible
type trait suggests this is not possible, because it is defined in terms of declval
:
The predicate condition for a template specialization
is_constructible<T, Args...>
shall be satisfied if and only if the following variable definition would be well-formed for some invented variablet
:
T t(declval<Args>()...);
So in our example, the type trait states if Class
can be constructed from a hypothetical function that returns Class&&
. Whether the the line in test()
is allowed cannot be predicted by any of the current type traits, despite the naming suggests that is_constructible
does.
This means, in all situations where guaranteed copy elision would actually save the day, is_constructible
misleads us by telling us the answer to "Would it be constructible in C++11?".
This is not limited to is_constructible
. Extend the example above with (view on godbolt.org)
void consume(Class) noexcept {}
void test2() {
consume(getClass()); // succeeds in C++17 because of guaranteed copy elision
}
static_assert(std::is_invocable<decltype(consume), Class>::value); // fails because move ctor is deleted
This shows that is_invocable
is similarly affected.
The most straightforward solution to this would be to change declval
to
template<class T> T declval_cpp17() noexcept;
Is this a defect in the C++17 (and subsequent, i.e. C++20) standard? Or am I missing a point why these declval
, is_constructible
and is_invocable
specifications are still the best solution we can have?
Class c2(c1);
fails. Why shouldis_constructible
say it doesn't? In your example you're not constructing a new object at all. – AbundanceClass c2(c1)
is trying to construct fromClass&
(orClass const&
). And this would be denied correctly byis_constructible
– Holyheadc1
isClass
, notClass&
. – Abundancec1
is. But the type of the expression is a reference. – Holyhead