Why is initialization of a constant dependent type in a template parameter list disallowed by the standard?
Asked Answered
P

1

35

In the answer to this post "(Partially) specializing a non-type template parameter of dependent type", it states:

The type of a template parameter corresponding to a specialized non-type argument shall not be dependent on a parameter of the specialization. [ Example:

template <class T, T t> struct C {};
template <class T> struct C<T, 1>; // error

template< int X, int (*array_ptr)[X] > class A {};
int array[5];
template< int X > class A<X,&array> { }; // error

—end example ]

My question is why is this restriction here? There is at least one use case where I find that this restriction interferes with writing clean code. E.g.

template <typename T, T*>
struct test;

template <typename T>
struct test<T, nullptr> // or struct test<T, (T*)nullptr>
{
};

template <typename R, typename...ARGs, R(*fn)(ARGs...)>
struct test<R(ARGs...), fn>
{
};

Though I'm unsure if there are other cases that stating a constant based on a type is a problem beyond not making any sense.

Anyone have a reason for why this is so?

Pinna answered 19/8, 2015 at 20:17 Comment(15)
I mean I think it could make sense if you have a bunch of types that have e.g. static const int value = 42; and some other types that have static const char value = 'a', then potentially you could partially specialize a template based on <typename T, T::v> ... I can't think of any particular reason for the restriction, I don't think allowing that kind of substitution and pattern matching would be harder than the kind they already do. But maybe I'm missing something.Iorgos
FWIW there may be no real answer to this. The standard also says "you shall not take a pointer to a constructor" and afaik there's no particularly compelling reason for this restrictionIorgos
@ChrisBeck That's more reasonable and understandable. A constructor requires memory for its object, thus hidden code is needed prior to the call. This allocation can be directly before the call, or anywhere else prior, which is compiler defined. To call a constructor without saying where to store the object is meaningless so a pointer to a constructor is more than just meaningless, it's downright dangerous. Inplace new allows for the programmer to specify the memory location if such fine grained use is actually warranted and factory functions can be generated in place of a constructor.Pinna
@ChrisBeck, the standard doesn't usually make things up arbitrarily. The only one I'm aware of is the non-existence of member function types and references, and that is still more arguable than this AFAICT (though it still kind of still bugs me every now and again o.O).Pinna
I guess what I would want is for the standard to give me a pointer to an automatically generated corresponding factory function when I ask for the pointer to the constructor. Ofc it's not really that important anyways. If it were possible for me to use pointers to constructors as template parameters I think it would be sometimes quite usefulIorgos
twitter hashtag #off-topicIorgos
@ChrisBeck This would be your function: template <typename CLASS, typename...ARGs> CLASS factory(ARGs&&...args) { return CLASS(std::forward<ARGs...>(args...)); } your function pointer would be auto pConstrutor = &factory<class_name, arg0_t, ..., argN_t>, and your call would be class_name var = pConstructor(arg0, ..., argN). And that will be my last post for this OT threadPinna
Adrian: that is a very good point. I was reminded of this exchange while reading this: stroustrup.com/bs_faq2.html#overload-dot Bjarne points out that there's no particular reason that you can't overload the ternary operator. But fwiw I think I'm glad for thatIorgos
@ChrisBeck Note that I didn't test that and upon looking at it again, it is not quite right. But you get the idea.Pinna
This seems to have originated from N0668, but that's just the wording. Trying to see if I can track down the rationale.Thirdrate
@Thirdrate : From last page of N0668, they give an algorithm to determine which class partial template specializations are more specific than others, which basically treats the nontype parameters separately, and reuses the earlier algorithm for function templates for the type parameters. I guess that this doesn't work if the nontype parameters' types depend on the type parameters. So my assumption is that, the standards committee just felt it saved compiler writers a significant amount of work to disallow this? Does that sound reasonable?Iorgos
@ChrisBeck I'm not a C++ standards or template ellipsis expert, but the constraint feels natural. In your quoted examples, the partial specialization with one of the non-type parameters forces the type of the type-parameter T in the template to be of an implied type. It may even make the template non-partial. Reiterating the construct as if the type is unrelated from the non-type parameter, introduces new ambiguity as it seemingly allows the template construct to violate itself. Your last finding about the order in which template code is analyzed is probably the result of the same reasoning.Stouffer
@StarShine, are you talking to Chris or to me, the OP? If it is me, then I don't really understand your reasoning. How could this possibly make this non-partial, add any ambiguity or violate itself?Pinna
@Pinna I was following up on Chris' reaction. Let's say the non-type parameter is of a numeral primitive type, e.g. 'int'. According to the declaration, first type parameter T can only be of that same type. Leaving the type template parameter T unspecified would allow for class types that violate the template type association in the declaration. Instead of having the compiler find out about possible violations by instancing all required cases, it is faster to warn about this up-front. Not much is lost. There was no need for template typing. I believe type-traits can help you in these cases.Stouffer
@StarShine, are you indexing from 0? Yes, I know that T has to have the same type as T. According to the template example definition that I specified, T must be specified. I think I'm not understanding you. Perhaps an example of what you mean?Pinna
B
5

(IMHO) The most common reasons the standard disallows a specific feature are:

  1. The feature is covered by another mechanism in the language, rendering it superfluous.
  2. It contradicts existing language logic and implementation, making its implementation potentially code breaking.
  3. Legacy: the feature was left out in the first place and now we've built a lot without it that it's almost forgotten (see partial function template specialization).

Difficulty of implementation is rarely a factor, though it may take some time for compiler implementations to catch up with evolution on the "hard" stuff.

You could always wrap your non type template parameter in another type:

template < typename T1, typename T2 > 
struct Demo {}; // primary template

template < typename T > 
struct Demo<T, integral_constant<T, 0>> {}; // specialization

I doubt this hack falls into case 1. Case 3 is always a possibility so lets examine case 2. To do this, we have to know which are the related rules the standard imposes on class templates partial specializations.

14.5.5 Class template partial specializations

  1. A non-type argument is non-specialized if it is the name of a non-type parameter. All other non-type arguments are specialized. (C1)

  2. Within the argument list of a class template partial specialization, the following restrictions apply:

    • A partially specialized non-type argument expression shall not involve a template parameter of the partial specialization except when the argument expression is a simple identifier. (C2)
    • The type of a template parameter corresponding to a specialized non-type argument shall not be dependent on a parameter of the specialization. (C3)

I marked the first three Clauses I found relevant (the third is the one in question). According to C1 in our case we have a specialized non-type argument so C2 should stand, yet this

template <class T, T t> struct C {};
template <class T> struct C<T, 1>;

is actually

template <class T, T t> struct C {};
template <class T> struct C<T, T(1)>; // notice the value initialization

so the partially specialized non type argument T t involves the template parameter of the partial specialization class T in an expression other than an identifier; furthermore such specializations are bound to involve class T in a value initialization which will always be a violation of the rules. Then C3 comes along and clears that out for us so that we won't have to make that deduction every time.

So far we've established that the rules are in sync with themselves but this does NOT prove case 2 (once we remove the initial limitation every other related limitation falls apart). We'd have to dive into matching class template partial specializations rules; partial ordering is considered out of scope here because if we can produce valid candidates it's up to the programmer to put together a well formed program (i.e. not create ambiguous uses of class templates).

Section

Matching of class template partial specializations [temp.class.spec.match]

describes the (give or take) "pattern matching" process involved in template specialization. Rule 1 is the overall workflow of the procedure and the subsequent rules are those that define correctness

  1. A partial specialization matches a given actual template argument list if the template arguments of the partial specialization can be deduced from the actual template argument list

  2. A non-type template argument can also be deduced from the value of an actual template argument of a non-type parameter of the primary template.

  3. In a type name that refers to a class template specialization, (e.g., A) the argument list shall match the template parameter list of the primary template. The template arguments of a specialization are deduced from the arguments of the primary template.

These rules are not violated by allowing the type of a template parameter corresponding to a specialized non-type argument to be dependent on a parameter of the specialization. So IMHO there is no specific reason why we can't have this feature in future revisions of the language: legacy is to blame. Sadly I didn't manage to find any language proposals with the initiative to introduce this feature.

Beech answered 6/9, 2015 at 18:5 Comment(3)
furthermore such specializations are bound to involve class T` in a value initialization which will always be a violation of the rules` - how so? Would not constexpr constructors make this not a problem?Pinna
@Pinna I can't see how constexpr is relevant here. The argument expression is still not a simple identifierBeech
Oh yes, that right. I was confusing your statement with my example where it was template <typename T, T* p> which would have been as it is a pointer to a type and not a type itself. So legacy is blame and there is no reason for this. Hmmmm.Pinna

© 2022 - 2024 — McMap. All rights reserved.