The toughest of the challenges, giving a single function evaluating whether a constexpr
constructor from const T&
exists for arbitrary T, given here seems hardly possible in C++17. Luckily, we can get a long way without. The reasoning for this goes as follows:
Knowing the problem space
The following restrictions are important for determining if some expression can be evaluated in constexpr
content:
To evaluate the copy constructor of T
, a value of type const T&
is needed. Such a value must refer to an object with active lifetime, i.e. in constexpr
context it must refer to some value created in a logically enclosing expression.
In order to create this reference as a result of temporary promotion for arbitrary T
as we would need to know and call a constructor, whose arguments could involve virtually arbitrary other expressions whose constexpr
-ness we would need to evaluate. This looks like it requires solving the general problem of determining the constexpr
ness of general expressions, as far as I can understand. ¹
¹ Actually, If any constructor with arguments, including the copy constructor, is defined as constexpr
, there must be some valid way of constructing a T
, either as aggregate initialization or through a constructor. Otherwise, the program would be ill-formed, as can be determined by the requirements of the constexpr
specifier §10.1.5.5:
For a constexpr function or constexpr constructor that is neither defaulted nor a template, if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, a constant initializer for some object ([basic.start.static]), the program is ill-formed, no diagnostic required.
This might give us a tiny loophole.²
So the expression best be an unevaluated operand §8.2.3.1
In some contexts, unevaluated operands appear ([expr.prim.req], [expr.typeid], [expr.sizeof], [expr.unary.noexcept], [dcl.type.simple], [temp]).
An unevaluated operand is not evaluated
Unevaluated operands are general expressions but they can not be required to be evaluatable at compile time as they are not evaluated at all. Note that the parameters of a template are not part of the unevaluated expressiont itself but rather part of the unqualified id naming the template type. That was part of my original confusion and tries in finding a possible implementation.
Non-type template arguments are required to be constant expressions §8.6 but this property is defined through evaluation (which we have already determined to not be generally possible). §8.6.2
An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would [highlight by myself] evaluate one of the following expressions:
Using noexpect
for the unevaluated context has the same problem: The best discriminator, inferred noexceptness, works only on function calls which can be evaluated as a core-constant expression, so the trick mentionend in this stackoverflow answer does not work.
sizeof
has the same problems as decltype
. Things may change with concepts
.
The newly introduced if constexpr
is, sadly, not an expression but a statement with an expression argument. It can therefore not help enforce the constexpr
evaluatability of an expression. When the statement is evaluated, so is its expression and we are back at the problem of creating an evaluatable const T&
. Discarded statements have not influence on the process at all.
Easy possibilities first
Since the hard part is creating const T&
, we simply do it for a small number of common but easily determined possibilities and leave the rest to specialization by extremely special case callers.
namespace detail {
template <int> using Sink = std::true_type;
template<typename T,bool SFINAE=true> struct ConstexprDefault;
template<typename T>
struct ConstexprDefault<T, Sink<(T{}, 0)>::value> { inline static constexpr T instance = {}; };
template<typename T> constexpr auto constexpr_copiable(int) -> Sink<(T{ConstexprDefault<T>::instance}, 0)>;
template<typename T> constexpr auto constexpr_copiable(...) -> std::false_type;
}
template<typename T>
using is_constexpr_copyable_t = decltype(detail::constexpr_copiable<T>(0));
Specializing details::ConstexprDefault
must be possible for any class type declaring a constexpr copy constructor, as seen above. Note that the argument does not hold for other compound types which don't have constructors §6.7.2. Arrays, unions, references and enumerations need special considerations.
A 'test suite' with a multitude of types can be found on godbolt. A big thank you goes to reddit user /u/dodheim from whom I have copied it. Additional specializations for the missing compound types are left as an exercise to the reader.
² or What does this leave us with?
Evaluation failure in template arguments is not fatal. SFINAE makes it possible to cover a wide range of possible constructors. The rest of this section is purely theoretical, not nice to compilers and might otherwise be plainly stupid.
It is potentially possible to enumerate many constructors of a type using methods similar to magic_get
. Essentially, use a type Ubiq
pretending to be convertible to all other types to fake your way through decltype(T{ ubiq<I>()... })
where I
is a parameter pack with the currently inspected initializer item count and template<size_t i> Ubiq ubiq()
just builds the correct amount of instances. Of course in this case the cast to T
would need to be explicitely disallowed.
Why only many? As before, some constexpr constructor will exist but it might have access restrictions. This would give a false positive in our templating machine and lead to infinite search, and at some time the compiler would die :/. Or the constructor might be hidden by an overload which can not be resolved as Ubiq
is too general. Same effect, sad compiler and a furious PETC
(People for the ethical treatment of compilers™, not a real organization). Actually, access restrictions might be solvable by the fact that those do not apply in template arguments which may allow us to extract a pointer-to-member and [...].
I'll stop here. As far as I can tell, it's tedious and mostly unecessary. Surely, covering possible constructor invocations up 5 arguments will be enough for most use cases. Arbitrary T
is very, very hard and we may as well wait for C++20 as template metaprogramming is once again about to change massively.
-std=c++1z
– Vittorio RomeoT(T())
isn't a copy anyway. It's exactly equivalent toT()
. – Barry-std=c++1z
if you add a deleted move constructor inBar
. In this case, GCC compiles it and clang fails to compile it. – skypjackSink<(T(static_cast<const T &>(T{})),0)>
work around it? GCC and clang still disagree i fusing-std=c++1z
, but it seems that this way it gets back in the example the copy. Am I wrong? – skypjack