Why is substitution failure in parameter mappings considered ill-formed? - c++

In this code,
template<class T, class U>
concept always_true = true;
template<class T>
concept always_true_if_tagged = always_true<T, typename T::tag>;
struct A {
using tag = int;
};
static_assert(always_true_if_tagged<A>);
static_assert(!always_true_if_tagged<int>); //GCC says this failed
GCC says that the second assertion failed. Both Clang and MSVC are agree to compile it.
I originally thought that it is ill-form with no diagnostic required, because of temp.constr.normal#1.4
The normal form of a concept-id C<A1, A2, ..., An> is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required.
The substitution T::typename tag is a parameter mapping for always_true, so it is ill-formed; no diagnostic is required.
So my first two questions are
Was I correct? (Is it ill-formed and did I cite the correct reason?)
Why it should be ill-formed? (If I was correct.)
One of the solutions is to check the nested typename before. So the parameter mapping for always_true doesn't happen.
template<class T>
concept always_true_if_tagged =
requires {typename T::tag;}
&& always_true<T, typename T::tag>;
Furthermore, temp.constr.atomic#3 says
To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied. Otherwise, the lvalue-to-rvalue conversion is performed if necessary, and E shall be a constant expression of type bool. The constraint is satisfied if and only if evaluation of E results in true. If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required.
If I write
template<class T>
concept always_true_if_tagged = bool(always_true<T, typename T::tag>);
bool(always_true<T, typename T::tag>) is an atomic constraint IIUC. The substitution of T::typename tag with T=int results in invalid type, so it should be well-formed and not satisfied.
So my last two(or four) questions are
Why this doesn't apply to the first code or why [temp.constr.normal#1.4] doesn't apply here?
3.1. Is this substitution a parameter mapping for concept-id always_true?
3.2. Is the usage of always_true in concept C = always_true<T, typename T::tag> an atomic contraint? temp.constr.constr#general-1 says there are three different kinds of constraints: conjunctions, disjunctions and atomic constraints.
Why can't concept C = always_true<T, typename T::tag> with T=int be well-formed like this one? (Likely the same as the second question though)

First of all, the lhs of scope resolution operator (::) can only be "a namespace or a class, enumeration, or dependent type", ill-formed otherwise.
With my experiment, always_true_if_tagged<int> is actually evaluate to true in gcc compiler, so the static_assert failed is because of evaluating to false. I guess that gcc compiler assumed that regardless of T, U, always_true is always true, so optimized out the evaluation of typename T::tag in constraint normalization.
So my answers to your questions:
I believe that you are correct.
The compiler has no idea whether the constraint is satisfied in this state (constraint normalization), and cannot perform further evaluation until a concept returns true or false.
As descibe above, I think compiler optimzed out the evaluation.
Subtitution is happend when concept is used, and parameter mapping is "created" when concept is declared, see follow.
Atomic constraint
An atomic constraint consists of an expression E and a mapping from the template parameters that appear within E to template arguments involving the template parameters of the constrained entity, called its parameter mapping.
I believe you have the answer now.

Were you correct? […]
Yes typename T::tag doesn’t name a valid type with [T = int].
Why should it be ill-formed?
The reason you cited is right. typename int::tag is just non-sense.
From memory, you’d also need to move the template specialization below the struct A to use typename A::tag in the template.
Why […]
I don’t believe this is well-formed because of §7.3.2 [conv.lval]
I don’t have the answer to the questions, only more questions.
In a concept, it’s not a template instantiation (See: [temp.concept], [temp.spec], [temp.dep]), so I don’t understand how that could be an atomic constraint (is it a constant expression? How can it evaluate to anything).
clang 14.0.0 doesn’t generate warnings with -fsanitize=undefined on 😰
See also:
SFINAE

Related

C++20 Concepts: Constraint Normalization

This is an example from the C++20 Standard (ISO/IEC 14882:2020), Section 13.5.4 ([temp.constr.normal]), Paragraph 1 (emphasis mine):
The normal form of a concept-id C<A1 , A2 , ..., An> is the normal form of the constraint-expression of C, after substituting A1 , A2 , ..., An for C’s respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required.
template<typename T> concept A = T::value || true;
template<typename U> concept B = A<U*>;
template<typename V> concept C = B<V&>;
Normalization of B’s constraint-expression is valid and results in T::value (with the mapping T -> U*) V true (with an empty mapping), despite the expression T::value being ill-formed for a pointer type T. Normalization of C’s constraint-expression results in the program being ill-formed, because it would form the invalid type V&* in the parameter mapping.
I understand that C makes a program ill-formed (and why). However, it is not clear to me if B would result in the program being ill-formed or not. The text states that B's normalization is valid, but at the same time it states that the expression T::value is ill-formed due to that pointer type (which I understand). Does it mean that only the normalization part of the process is valid but the program itself is ill-formed in a later stage when checking T::value? Or is the program valid in any case and the check of T::value is skipped/avoided somehow?
I checked with Godbolt's Compiler Explorer and both GCC and Clang seem to be fine with this. Nevertheless, since the Standard says "no diagnostic is required", this does not help much.
The concept B is valid, as you can pass a pointer to the concept A. Inside A itself the pointer cannot access ::value but this, according to the spec, [temp.constr.atomic], would not be considered as an error but rather as false, then the || true on concept A would make the entire expression true.
Note that if we pass int& to concept B then our code would be IFNDR, as B would try to pass to A an invalid type (int&*).
The concept C is IFNDR as is, since it passes a reference to B, that tries to pass a pointer to this reference to A, and again a V&* type is invalid.
Trying to evaluate an ill-formed-no-diagnostic-required expression using static_assert will not necessarily help in answering the question whether the expression is valid or not, as the compiler is not required to fail a static_assert on an ill-formed-no-diagnostic-required expression.
Note that each normalized constraint consists out of 2 parts:
The atomic constraint and an associated parameter mapping.
Let's separate each constraint into those two parts for your three example concepts:
In your example the normalized form of concept A would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ T
Atomic expression: true
No Parameter Mapping
The normalized form of concept B would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ U*
Atomic expression: true
No Parameter Mapping
And the normalized form of concept C would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ V&*
Atomic expression: true
No Parameter Mapping
How the parameter mappings get formed
Forming the parameter mapping for an atomic expression is straightforward:
By default an atomic expression always starts out with an identity parameter mapping (i.e. no type modifications):
13.5.4 Constraint normalization [[temp.constr.normal]]
(1) The normal form of an expression E is a constraint that is defined as follows:
[...]
(1.5) The normal form of any other expression E is the atomic constraint whose expression is E and whose parameter mapping is the identity mapping.
and the only way to get a non-identity parameter mapping is to name another concept within the constraint:
13.5.4 Constraint normalization [[temp.constr.normal]]
(1.4) The normal form of a concept-id C<A1, A2, ..., An> is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. [...]
Here are a few examples:
template<class T> constexpr bool always_true = true;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U (identity)
template<class U> concept Foo = Base<U>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V::type
template<class V> concept Bar = Base<typename V::type>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&&
template<class W> concept Baz = Base<W&&>;
Your quoted section
Which brings us back to your original quoted section:
13.5.4 Constraint normalization [[temp.constr.normal]]
(1.4) The normal form of a concept-id C<A1, A2, ..., An> is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required.
Note that the highlighted statement only applies to the parameter mapping - not to the atomic expression itself.
This is why concept C in your example is ill-formed, NDR - because the parameter mapping for its atomic expressions forms an invalid type (pointer to reference): X ↦ V&*
Note that the actual type that gets substituted for V does not matter at the normalization stage; the only thing that matters is if the mapping itself forms an invalid type or expression.
Here are a few more examples:
template<class T> constexpr bool always_true = true;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U::type
template<class U> concept Foo = Base<typename U::type>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V*::type
template<class V> concept Bar = Foo<V*>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&*
template<class W> concept Baz = Foo<W&>;
Rough Timeline of events during compilation
To answer the question of when your program gets ill-formed ndr, we need to establish the order in which events take place during compilation.
When the constraints of an associated declaration are determined OR the value of a concept is evaluated then constraint normalization will take place.
This is given by:
13.5.4 Constraint normalization [[temp.constr.normal]]
[Note 1] Normalization of constraint-expressions is performed when determining the associated constraints of a declaration and when evaluating the value of an id-expression that names a concept specialization.
This is where your program would become ill-formed, ndr if the parameter mapping forms an invalid type or expression.
After the constraints have been normalized the actual type will be substituted into the constraints:
13.5.2.3 Atomic constraints [[temp.constr.atomic]]
(3) To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied.
Note that at this point it is allowed for invalid types or expression to be formed - if that's the case then the result of the constraint will simply be false.
Conclusion
So to answer your questions:
Does it mean that only the normalization part of the process is valid but the program itself is ill-formed in a later stage when checking T::value?
Concepts A and B are well-formed.
Concept C is ill-formed, ndr during the normalization process.
The actual atomic constraint T::value does not matter in this case; it could as well simply be always_true<T>.
Or is the program valid in any case and the check of T::value is skipped/avoided somehow?
The program is valid as long as concept C never gets normalized.
i.e. explicitly evaluating it or using it as a constraint would make your program ill-formed, ndr.
Example:
// evaluates concept C
// -> results in normalization of C
// -> ill-formed, ndr
static_assert(C</* something */>);
template<C T>
void foo() {}
// constraints of foo will be determined
// -> results in normalization of C
// -> ill-formed, ndr
foo</* something */>();

Is concept a variant of SFINAE

SFINAE is a technology that permits invalid expressions and/or types in the immediate context of a templated function while the concept seems to have the same effect since we are only permitted to use expressions and types(in requires-expression) in the constraint-expression of the concept-definition, and the constraint-expression is true if all expressions and/or types are valid, and false otherwise. It appears to me that concept cannot do anything that exceeds what SFINAE can do, the same is true the other way around.
Is my understanding right? If it is not, what is the case in which we can only by using the concept, or by using SFINAE but using the other way would result in an error? If there does not exist such a scene, is the concept merely a graceful/modern way of using the technology SFINAE(i.e. they are essentially equivalent)?
Update:
IMO, the only difference is, certain expressions and types we want to check are dispersed in the declaration for SFINAE, by contrast, certain expressions and types we want to check are collected in the constraint-expression of the single concept-definition and use the declared concept in the declaration. Their effect essentially relies on invalid expressions and types.
They are not equivalent. Concepts can appear in more places and are partially ordered by subsumption. Some examples:
1. Concept subsumption may be used to rank overloads. With SFINAE, this is an error:
template <typename T>
auto overload(T) -> std::enable_if_t<std::is_copy_constructible_v<T>>;
template <typename T>
auto overload(T) -> std::enable_if_t<std::is_move_constructible_v<T>>;
void test() {
overload(1); // error: ambiguous
}
With concepts, this works:
void overload(std::copy_constructible auto);
void overload(std::move_constructible auto);
void test() {
overload(1);
}
2. Similarly, concept subsumption may be used to rank partial specializations.
3. Concepts are allowed on non-template member functions, so they can constrain special member functions.
Since a copy constructor is not a template, SFINAE never applies. When one needs conditional behavior before concepts (e.g. trivial copy constructor if the template argument of the class template is itself trivial), one had to conditionally introduce different base classes.
4. Concepts can constrain deduction.
One can statically assert that a type returned satisfies your requirements without asserting the precise type.
std::integral auto i = 1;
5. Concepts can be used in abbreviated function templates.
void f(std::integral auto);

Can a concept evaluation depend on where it is evaluated?

[temp.concept]/5 says:
A concept is not instantiated ([temp.spec]).
[ Note: An id-expression that denotes a concept specialization is evaluated as an expression ([expr.prim.id]). [...]]
Does it mean that this rule bellow ([temp.point]/8) does not apply?
If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.
For example if this rule does not apply, this code bellow is well formed:
template<class T>
concept Complete = sizeof(T)==sizeof(T);
struct A;
constexpr inline bool b1 = Complete<A>; //Complete<A>==false;
struct A{};
constexpr inline bool b2 = Complete<A>; //Complete<A>==true;
This question is followed by this one
Can a concept evaluation depend on where it is evaluated?
No.
It used to be the case that this was true (as my answer before this edit stated), but it turns out that this severely inhibits compiler throughput (since you cannot cache the result of a concept check) and the motivation for having it to begin with was pretty weak. This was a very late change, adopted as part of P2104 in the Prague 2020 meeting which adds the following sentence to [temp.constr.atomic]:
If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required.
As a result, this:
template<class T>
concept Complete = sizeof(T) == sizeof(T);
struct A;
static_assert(!Complete<A>);
struct A {};
static_assert(Complete<A>);
is ill-formed, NDR (practically speaking, Complete<A> will still be false after A becomes complete). In other words, we "memoize" concepts in the same way we "memoize" template instantiations.

What is the correct result of std::is_constructible<void()>::value?

I'm getting inconsistent results for std::is_constructible<void()>::value. My interpretation of the standard is that it should be false. However, Clang, with both libc++ and libstdc++*, gives true. GCC and MSVC both give false. Which result is correct?
Standardese
Here is the standardese, N4527 [meta.unary.prop]/7:
Given the following function declaration:
template <class T> add_rvalue_reference_t<T> create() noexcept;
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
variable t:
T t(create<Args>()...);
Note: This text changed slightly from C++11 (N3485), where create was not marked noexcept. However, the results of my tests did not change when accounting for this.
Test Case
Here is my minimal test case of both the type trait and the standardese definition:
#include <type_traits>
static_assert(std::is_constructible<void()>::value, "assertion fired");
template<typename T>
std::add_rvalue_reference_t<T> create() noexcept;
template<typename T, typename... Args>
void foo() {
T t(create<Args>()...);
}
int main() {
foo<void()>();
}
Results:
Clang (HEAD, libc++):
static assertion PASSED
foo<void()> did NOT compile
Clang (HEAD, libstdc++)*:
static assertion PASSED
foo<void()> did NOT compile
GCC (HEAD, libstdc++):
static assertion FAILED
foo<void()> did NOT compile
MSVC (version 19 via http://webcompiler.cloudapp.net/):
static assertion FAILED
foo<void()> did NOT compile (requires commenting out the static assertion)
*__GLIBCXX__ is not defined when Clang is used both with no -stdlib option and with -stdlib=libstdc++. I am unsure of whether libstdc++ is actually being used. If my interpretation of the standard is correct, then I am unsure of whether it is a bug with Clang or with libc++.
Keep reading. From the same paragraph:
Access checking is performed as if in a context unrelated to T and
any of the Args. Only the validity of the immediate context of the
variable initialization is considered. [ Note: The evaluation of the
initialization can result in side effects such as the instantiation of
class template specializations and function template specializations,
the generation of implicitly-defined functions, and so on. Such side
effects are not in the “immediate context” and can result in the
program being ill-formed. —end note ]
The assertion only fails when the template constructor is instantiated. However, as cleared up in the note, that assertion is not in the immediate context of the variable definition that is considered, and thus does not affect its "validity". So the compilers can count that definition as valid, even if actually attempting to construct a void() results in an ill-formed program.
Note that the compilers are also allowed to, instead of having is_constructible yield false, just reject the original program based on the assertion.

static member of class template error

I have a problem with this code snippet:
template <typename T>
struct S
{
static int a;
};
template <typename T>
decltype(S<T>::a) S<T>::a;
clang-3.4 says:
s.cpp:8:25: error: redefinition of 'a' with a different type: 'decltype(S<T>::a)' vs 'int'
decltype(S<T>::a) S<T>::a;
^
s.cpp:4:14: note: previous definition is here
static int a;
^
1 error generated.
But gcc-4.8.2 accepts. Which of the compilers is right? Should I avoid such code in the future?
Clang is demanding that the definition match the declaration at template definition time, whereas GCC and others defer matching until instantiation time (which never even happens for your example).
Clang accepts this:
#include <type_traits>
template <typename T>
struct S
{
static int a;
};
template <typename T>
typename std::enable_if< true, int >::type S<T>::a; // Resolves before instantiation
but rejects this small change:
template <typename T>
typename std::enable_if< std::is_same< T, T >::value, int >::type S<T>::a;
I cannot recall where the standard dictates when object declaration matching occurs, but I suspect that Clang is within its rights to reject the code. The intent of the standard, if I recall correctly, is that each declaration matches exactly one definition, and that mapping may be determined before instantiation time.
With the looser rule that GCC is apparently applying, you could have two member declarations and two definitions, but each definition may finalize either of the declarations depending on the template parameters.
The code which GCC and MSVC are accepting is ill-formed, no diagnosis required… pending finding the actual standardese buried somewhere in §3 [basic], §7 [dcl.dcl], §8 [dcl.decl], §14 [temp], or maybe somewhere else.
I still cannot find what rule matches object definitions to preceding declarations, but §14.4/2 dictates that decltype(…) cannot be the equivalent (I assume in the declarative sense) to int.
If an expression e involves a template parameter, decltype(e)
denotes a unique dependent type. Two such decltype-specifiers refer
to the same type only if their expressions are equivalent (14.5.6.1).
[Note: however, it may be aliased, e.g., by a typedef-name. — end
note ]
I'm pretty sure that equivalence, not mere aliasing, is necessary for the definition to match the declaration. §14.5.6.1 delves pretty deep into this territory, except it is specifically discussing function signatures.
I think Clang might be right in rejecting this. 14.2p2 says about decltype(e)
If an expression e involves a template parameter, decltype(e) denotes a unique dependent type.
In DR #2, the discussion trace says
My opinion (which I think matches several posted on the reflector recently) is that the out-of-class definition must match the declaration in the template.
...
In general, if you can match the declarations up using only information from the template, then the declaration is valid.
I think it still matches if one of them uses a typedef (as demonstrated in the DR), because S<T>::type is a member of the current instantiation and the type aliased can be looked up directly. But a decltype(e), as specified above, will always denote a unique type (during template parse time) except with respect to another decltype(e) that specifies an equivalent expression.
Why did I say might? Because of 14.6p8
No diagnostic shall be issued for a template for which a valid specialization can be generated.
One could read this as saying the type equivalence check is simply delayed till after instantiation. This, however, would contradict the discussion trace in the DR I think, because they say "if you can match the declarations up using only information from the template, then the declaration is valid" (and I assume the author of this statement meant to be exhaustive about the situations when the declaration is valid).
For me clang is broken here.
All combinations with decltype will fail. Without decltype it works.
template <typename T>
struct S
{
static int a;
using type = decltype( a );
typedef decltype( a ) type2;
};
template <typename T>
1) decltype(S<T>::a) S<T>::a;
2) int S<T>::a;
3) typename S<T>::type S<T>::a;
4) typename S<T>::type2 S<T>::a;
1 gcc works, clang fails
2 gcc + clang works
3 gcc works, clang fails
4 gcc works, clang fails
I did some more tries to work around the problem, but could not have any success.
There are some more discussions on that kind of problems:
C++ Static member initalization (template fun inside)
Edit:
I found that this topic is simply "not resolved" in the standard until now and clang did not implemented it:
Take a look at:
http://clang.llvm.org/cxx_dr_status.html ( point 205 )
I hope that I did not misunderstood the page. Feel free to correct my interpretation.