9

I think the following code is well-formed:

template< typename T >
using IsSigned = std::enable_if_t< std::is_signed_v< T > >;

template< typename T, IsSigned< T >... >
T myAbs( T val );

Others say that it is ill-formed, because §17.7 (8.3) of the C++17 standard:

Knowing which names are type names allows the syntax of every template to be checked. The program is ill-formed, no diagnostic required, if: (...) every valid specialization of a variadic template requires an empty template parameter pack, or (...)

In my opinion IsSigned< T >... is a dependent template parameter, therefore it can not be checked against §17.7 (8.3) in template definition time. IsSigned< T > could be for example void for one subset of Ts, int for another subset or substitution failure. For the void subset it is true, that the empty template parameter pack would be the only valid specialization, but the int subset could have many valid specializations. It depends on the actual T argument.

It means that the compiler must check it after the template instantiation, because T is not known before. At that point the full argument list is known, there is zero variadic arguments. The standard says the following (§17.6.3 (7)):

When N is zero, the instantiation of the expansion produces an empty list. Such an instantiation does not alter the syntactic interpretation of the enclosing construct

This is why I think it is well formed.

  • What do you think?
  • How can I track down this ambiguity for sure? It is hard to decide, because the code compiles but it means nothing: §17.7 (8.3) is NDR, the compilers do not have to raise any compilation error.
L. F.
  • 19,445
  • 8
  • 48
  • 82
Broothy
  • 659
  • 5
  • 20
  • In my opinion, arguments like "compiler must check it after" doesn't matter. The standard is clear. "every valid specialization of a variadic template requires an empty template parameter pack". If that's true for your code, then it is ill formed. Doesn't matter how the compiler chould check this in theory. – geza Sep 04 '19 at 07:51
  • I think the best course of action is to take the reason _why_ this rule is part of the standard and check whether it leads to problems in this instance. That's probably closest to the intent of the standard. – Max Langhof Sep 04 '19 at 08:01
  • @geza Sorry, `T` being `int` is of course not the correct case in the given code. I realize that I would more or less be restating the original question in light of that ("is the set of 'every valid specialization' for a template with dependent template parameters constrained by the constraints of the dependent template parameter or are those irrelevant?"), comment deleted. – Max Langhof Sep 04 '19 at 08:15
  • 1
    @MaxLanghof: yes, if there is a specialization, which makes `IsSigned` non-void, then the code is well-formed. But if there is no such specialization, then the code is ill-formed. At least, this is how I understand this, just strictly interpreting what's written. But I'm not sure, that this was the intent of the writers of the standard. – geza Sep 04 '19 at 08:16
  • 1
    @geza I think that interpretation might lead to code where the well-formedness is undecidable. Not sure if that's a problem... I tend towards the opposite interpretation (that the set of valid specializations is not constrained by the constraints of the dependent template parameters), so that even if no non-void specialization of `IsSigned` exists, it is well-formed (because such an `IsSigned` specialization _could_ exist). But I still think we should base answers on what the intent behind this rule is in the first place. – Max Langhof Sep 04 '19 at 08:20
  • @MaxLanghof: as we have a Turing complete meta language inside C++, this can easily happen :) – geza Sep 04 '19 at 08:26
  • In theory it would be easy to create such a specialization. This is why I think the reason was different behind this rule, for example to forbid constructs like this: `template< typename T1, typename T2, typename... TS > void f( std::pair< T1, T2, TS... > ); ` – Broothy Sep 04 '19 at 08:26

1 Answers1

9

The code is ill-formed, no diagnostic is required.

If std::is_signed_v<T>, then std::enable_if_t<std::is_signed_v<T>> denotes the type void. Otherwise, std::enable_if_t<std::is_signed_v<T>> does not denote a valid type. Therefore, every valid specialization of myAbs requires an empty template parameter pack.

Per [meta.rqmts]/4, the program has undefined behavior if std::enable_if is specialized. Therefore, the aforementioned behavior cannot be changed.

In my opinion IsSigned< T >... is a dependent template parameter, therefore it can not be checked against §17.7 (8.3) in template definition time. IsSigned< T > could be for example void for one subset of Ts, int for another subset or substitution failure. For the void subset it is true, that the empty template parameter pack would be the only valid specialization, but the int subset could have many valid specializations. It depends on the actual T argument.

The compiler cannot check it, in the same way it cannot, say, solve an arbitrary equation for you. NDR (no diagnostic required) is made exactly for such cases — the program is ill-formed and would require a diagnostic if the compiler is actually capable of detecting that. NDR permits the compiler not to check it.

When N is zero, the instantiation of the expansion produces an empty list. Such an instantiation does not alter the syntactic interpretation of the enclosing construct.

The rule we are talking about is a semantic rule, not a syntactic rule, because syntactic rules are in [gram].


So what is the rationale for the NDR rules? In general, they address problems that are not reproducible among implementation strategies. For example, they may cause the code to misbehave in some implementation strategies, but do not cause any problems (and cannot be easily) in others.


Also, note that the standard talks in terms of program with terms like "ill-formed". Therefore, it is not always plausible to talk about the well-formed ness of an isolated code snippet. In this case, std::enable_if is required not to be specialized, but the situation may get more complicated otherwise.

L. F.
  • 19,445
  • 8
  • 48
  • 82
  • Suppose a lib provides the function above. We state it is ill-formed. Then the user adds the following code: ```namespace std { template<> struct enable_if< true, void > { typedef int type; }; }``` It becomes well formed. I think we can not say it is well formed or ill formed before the instantiation. – Broothy Sep 04 '19 at 10:33
  • 1
    @Broothy The user code is undefined behavior per [\[meta.rqmts\]](http://eel.is/c++draft/meta.rqmts#4). – L. F. Sep 04 '19 at 10:38
  • @L.F. That is correct for the specific case of `std::enable_if` (which is an unfortunate choice for the question), but that is not relevant for the underlying question. The answer to the question cannot change depending on whether adding specializations is allowed for `std::enable_if` because you can ask the same question using a custom-written `enable_if` that specializations could be added for (and the rules in question are of course not specialized for `std` facilities). – Max Langhof Sep 04 '19 at 10:44
  • @MaxLanghof What is the underlying question? A library shouldn't provide a function like this without imposing requirements. (It is definitely legit for a library to say that your program is undefined behavior if you violate the requirements - at least that's how templates work in C++ currently.) – L. F. Sep 04 '19 at 10:49
  • @L.F. I see, you are right. As far as I understand if I replace `std::enable_if` and `std::is_signed` with a custom implementation that behaves exactly as the original, but resolves to int for a special, private enum the code would be well formed? – Broothy Sep 04 '19 at 10:51
  • 1
    The underlying question is "is `template struct DefaultVoid { using T = void; }; template using DefaultVoidT = DefaultVoid; template...> void foo(T) {};` well-formed or not?", with the implied "if not, would it be well formed after adding `template<> struct DefaultVoid { using T = int; }`?". I (or @Broothy) can ask a new question if you want, but the "don't specialize `std` stuff unless allowed" is completely irrelevant to the core question (also see the comment right above). – Max Langhof Sep 04 '19 at 10:53
  • @MaxLanghof No, the question apparently uses `std::enable_if_t`. This makes a big difference. That's also [mre]s are for - they prevent improper speculating. In this case, the OP provided a [mre], so I address it. I will be happy to answer if you ask the new question – L. F. Sep 04 '19 at 10:53
  • @Broothy The problem is that the standard doesn't have the notion of libraries or something like that - it considers whole programs when talking about things like "ill-formed". As you see, the standard always says "the program is ill-formed if you do `foo`" instead of "`foo` is ill-formed" – L. F. Sep 04 '19 at 10:55
  • 1
    With that addition you have my upvote! So as a consequence, this program would become well-formed if we added an intermediate template that simply wraps `std::enable_if`/`std::enable_if_t` (where the intermediate template could theoretically be specialized but which the library forbids anyone from specializing)? – Max Langhof Sep 04 '19 at 11:02
  • 1
    @MaxLanghof Yes. Although "well-formed" can fall into the "undefined behavior" category defined by the library if there's rule forbidding specialization :) – L. F. Sep 04 '19 at 11:04