SFINAE in variadic templates?

905 views
Skip to first unread message

Jim Porter

unread,
Feb 1, 2016, 4:26:26 AM2/1/16
to std-dis...@isocpp.org
I originally asked this on the clang mailing list, but didn't get a
response. Perhaps this would be a better venue.

The following code attempts to hide "func" via enable_if. Is it valid?

template<typename T> struct always_false : std::false_type {};
template<typename T, std::enable_if_t<always_false<T>::value, int>...>
void func(T &) {}

Specifically, I'm wondering about the use of std::enable_if being used
in the context of a non-type template parameter pack (in this case, a
pack of ints). If valid, it's a neat trick; in fact, it's already in use
in libstdc++'s implementation of <experimental/optional>. However, clang
doesn't understand it and SFINAE is never triggered, presumably because
it expands it to a pack of zero size before evaluating the enable_if.

My reading of [temp.deduct] p7 seems to suggest that clang is in the
wrong here: "The substitution [from template argument deduction] occurs
in *all* types and expressions that are used in the function type and in
template parameter declarations." (Emphasis mine.) However, I'm not
enough of a language lawyer to be certain. Can anyone confirm whether
this is a valid construct?

Thanks,
Jim

Richard Smith

unread,
Feb 1, 2016, 5:39:09 PM2/1/16
to std-dis...@isocpp.org
On Mon, Feb 1, 2016 at 1:26 AM, Jim Porter <jvp...@g.rit.edu> wrote:
> I originally asked this on the clang mailing list, but didn't get a
> response. Perhaps this would be a better venue.
>
> The following code attempts to hide "func" via enable_if. Is it valid?
>
> template<typename T> struct always_false : std::false_type {};
> template<typename T, std::enable_if_t<always_false<T>::value, int>...>
> void func(T &) {}

I assume you intended

template<typename ...T, std::enable_if_t<always_false<T>::value, int>...>
void func(T &...) {}

... otherwise the answer is that this is ill-formed, as it attempts to
expand a non-pack.

Your example also violates [temp.param]/11:

"A template parameter pack of a function template shall not be
followed by another template parameter unless that template parameter
can be deduced from the parameter-type-list of the function template
or has a default argument (14.8.2)."

Your enable_if_t pack is not deducible and has no default argument.

> Specifically, I'm wondering about the use of std::enable_if being used in
> the context of a non-type template parameter pack (in this case, a pack of
> ints). If valid, it's a neat trick; in fact, it's already in use in
> libstdc++'s implementation of <experimental/optional>. However, clang
> doesn't understand it and SFINAE is never triggered, presumably because it
> expands it to a pack of zero size before evaluating the enable_if.
>
> My reading of [temp.deduct] p7 seems to suggest that clang is in the wrong
> here: "The substitution [from template argument deduction] occurs in *all*
> types and expressions that are used in the function type and in template
> parameter declarations." (Emphasis mine.) However, I'm not enough of a
> language lawyer to be certain. Can anyone confirm whether this is a valid
> construct?

Substitution into a pack expansion makes N copies of the pattern and
substitutes into each of those copies; see [temp.variadic]/3 and /7.
If N == 0, no substitution into the pack is performed. However, that's
not the complete story, as there clearly are cases where some
substitution into a possibly-empty pack is performed, without
expanding the pack:

template<typename T> struct X {
template<T ...V> struct Y {};
};
X<X<int>> x;

This case is ill-formed by [temp.res]/8, as every valid specialization
of X<X<int>>::Y requires an empty parameter pack. It's at best unclear
from the standard which phases of substitution into a function
template should perform pack expansion. It's probably worth filing a
core issue asking for some clarification here.

Jim Porter

unread,
Feb 1, 2016, 7:03:53 PM2/1/16
to std-dis...@isocpp.org
On 2/1/2016 4:39 PM, Richard Smith wrote:
> On Mon, Feb 1, 2016 at 1:26 AM, Jim Porter <jvp...@g.rit.edu> wrote:
>> I originally asked this on the clang mailing list, but didn't get a
>> response. Perhaps this would be a better venue.
>>
>> The following code attempts to hide "func" via enable_if. Is it valid?
>>
>> template<typename T> struct always_false : std::false_type {};
>> template<typename T, std::enable_if_t<always_false<T>::value, int>...>
>> void func(T &) {}
>
> I assume you intended
>
> template<typename ...T, std::enable_if_t<always_false<T>::value, int>...>
> void func(T &...) {}

I didn't. The above is a slightly-simplified version of libstdc++'s
<experimental/optional> implementation[1]. The issue is also logged in
the LLVM Bugzilla[2]. Here's another example, compiling successfully
under GCC[3].

> ... otherwise the answer is that this is ill-formed, as it attempts to
> expand a non-pack.

The intention isn't to expand a non-pack, but to define a non-type
template parameter pack of type `int`, provided the condition passed to
enable_if_t is true. `std::enable_if_t<always_false<T>::value, int>`
should either evaluate to `int` or to SFINAE. In the former case, `func`
is just:

template<typename T, int...>
void func(T &) {}

That's surely valid code, but I'm not 100% sure if the template argument
deduction rules interact with SFINAE in a way that produces the expected
results.

- Jim

[1]
<https://gcc.gnu.org/viewcvs/gcc/trunk/libstdc%2B%2B-v3/include/experimental/optional?revision=232055&view=markup#l172>
[2] <https://llvm.org/bugs/show_bug.cgi?id=23840>
[3] <http://ideone.com/plkg5h>

Richard Smith

unread,
Feb 1, 2016, 7:40:39 PM2/1/16
to std-dis...@isocpp.org
Oh, sorry. Nonetheless, the same argument seems to apply: first we
deduce that T = <whatever> and your anonymous pack is empty, then we
substitute, and under [temp.variadic]/3 and /7, we generate an empty
list of template parameters into which we substitute T. I still think
it's worth filing a core issue on this, as the standard is unclear and
there is implementation divergence.

David Krauss

unread,
Feb 1, 2016, 11:35:23 PM2/1/16
to std-dis...@isocpp.org

On 2016–02–02, at 8:40 AM, Richard Smith <ric...@metafoo.co.uk> wrote:

Oh, sorry. Nonetheless, the same argument seems to apply: first we
deduce that T = <whatever> and your anonymous pack is empty, then we
substitute, and under [temp.variadic]/3 and /7, we generate an empty
list of template parameters into which we substitute T.

There’s no pack expansion in the example; it’s a declaration of a pack of yet-unknown length. See [temp.param] §14.1/15, “If a template-parameter is … a parameter- declaration that declares a parameter pack (8.3.5), then the template-parameter is a template parameter pack (14.5.3).” The pack is only deduced to be empty by [temp.arg.explicit] §14.8.1/3, “A trailing template parameter pack (14.5.3) not otherwise deduced will be deduced to an empty sequence of template arguments.” But by that point it must already exist, and SFINAE would seem to apply to the determination of T.

Richard Smith

unread,
Feb 2, 2016, 3:01:16 PM2/2/16
to std-dis...@isocpp.org
On Mon, Feb 1, 2016 at 8:35 PM, David Krauss <pot...@gmail.com> wrote:
>
> On 2016–02–02, at 8:40 AM, Richard Smith <ric...@metafoo.co.uk> wrote:
>
> Oh, sorry. Nonetheless, the same argument seems to apply: first we
> deduce that T = <whatever> and your anonymous pack is empty, then we
> substitute, and under [temp.variadic]/3 and /7, we generate an empty
> list of template parameters into which we substitute T.
>
>
> There’s no pack expansion in the example; it’s a declaration of a pack of
> yet-unknown length.

Per [temp.variadic]/4.21, a declaration of a parameter pack is a pack
expansion. The pack is expanded into a sequence of template arguments
when the deduced template arguments are substituted into it.

> The pack is only
> deduced to be empty by [temp.arg.explicit] §14.8.1/3, “A trailing template
> parameter pack (14.5.3) not otherwise deduced will be deduced to an empty
> sequence of template arguments.”

That wording is misplaced; as I recall, there's an existing core issue
on that. As that wording suggests, the pack is deduced to be empty
after the template argument deduction process and prior to
substitution. Substituting T = <whatever> and <anon> = <empty pack>
produces 0 template parameters, substitution into which is vacuous.

Richard Smith

unread,
Feb 2, 2016, 3:05:19 PM2/2/16
to std-dis...@isocpp.org
Consider this similar case:

void g(...);
template<typename T, typename ...U> void f(U ...u) { g(T::foo(u)...); }
void h() { f<int>(); }

Here, the simultaneous substitution of T = int and U = <empty pack>
into 'g(T::foo(u)...)' is valid, because the pattern is expanded zero
times. This seems analogous to the original case, assuming that only
one substitution step is performed to expand the template parameter
pack (as appears to be implied by the current wording).

David Krauss

unread,
Feb 3, 2016, 1:39:26 AM2/3/16
to std-dis...@isocpp.org

On 2016–02–03, at 4:01 AM, Richard Smith <ric...@metafoo.co.uk> wrote:

Per [temp.variadic]/4.21, a declaration of a parameter pack is a pack
expansion. The pack is expanded into a sequence of template arguments
when the deduced template arguments are substituted into it.

[temp.variadic]/4.2:
—  In a template parameter pack that is a pack expansion (14.1):
    — if the template parameter pack is a parameter-declaration; the pattern is the parameter-declaration without the ellipsis;
    — if the template parameter pack is a type-parameter with a template-parameter-list; the pattern is the corresponding type-parameter without the ellipsis.

This implies that some template-parameters are pack expansions. It doesn’t say that every template-parameter with an ellipsis is a pack expansion. Now you seem to be saying that a pack declared such as typename... or int... is intrinsically an expansion of a non-pack pattern to fit an argument list. Earlier, though, you mentioned that such a template-parameter “is ill-formed, as it attempts to expand a non-pack.”

A declaration of a parameter pack (that is, a template-parameter with an ellipsis before the optional identifier) is a pack expansion if and only if it contains an unexpanded pack. In this case, there is none.

That wording is misplaced; as I recall, there's an existing core issue
on that. As that wording suggests, the pack is deduced to be empty
after the template argument deduction process and prior to
substitution. Substituting T = <whatever> and <anon> = <empty pack>
produces 0 template parameters, substitution into which is vacuous.

There’s no T = <whatever> because substitution into <whatever> already failed. The substitution occurs before deduction when the parameter is explicitly specified.

 template<typename T, std::enable_if_t<always_false<T>::value, int>...>
 void func(T &) {}

 auto q = func<int>; // implementation variance

I suppose the paragraph might be better placed in the next section since it relates to deduction, but that doesn’t affect its meaning.

You might be thinking of [temp.deduct.call] §14.8.2.1/1:

For a function parameter pack that occurs at the end of the parameter-declaration-list, the type A of each remaining argument of the call is compared with the type P of the declarator-id of the function parameter pack. Each comparison deduces template arguments for subsequent positions in the template parameter packs expanded by the function parameter pack.

This wording should probably mention that it determines the length of the pack. (It would also be nice to clarify that the length is entirely determined by explicit specification ([temp.arg.explicit] §14.8.1/3), or by deduction (§14.8.2.*) at the end of a function parameter list. But again, the examples in this thread have no such deduction.)

Note that Clang will apply SFINAE to a pack declaration which generates no template arguments, if the pack length is in a deduced context:

template< typename, typename >
struct empty {};

template< typename e, typename ... t >
void f( e, empty< typename e::type, t > ... ) { std::cout << "#1\n"; }

template< typename e >
void f( e ) { std::cout << "#2\n"; }

auto p = f< int >; // OK, #2: SFINAE of empty pack declaration.
f( 3 ); // OK, #2: same, but after deduction.

(more on coliru)

Consider this similar case:

void g(...);
template<typename T, typename ...U> void f(U ...u) { g(T::foo(u)...); }
void h() { f<int>(); }

Here, the simultaneous substitution of T = int and U = <empty pack>
into 'g(T::foo(u)...)' is valid, because the pattern is expanded zero
times. This seems analogous to the original case, assuming that only
one substitution step is performed to expand the template parameter
pack (as appears to be implied by the current wording).

This involves the expansion of a function parameter pack u. Jim’s example contains no pack expansions at all.

Richard Smith

unread,
Feb 3, 2016, 2:24:00 PM2/3/16
to std-dis...@isocpp.org
On Tue, Feb 2, 2016 at 10:39 PM, David Krauss <pot...@gmail.com> wrote:

On 2016–02–03, at 4:01 AM, Richard Smith <ric...@metafoo.co.uk> wrote:

Per [temp.variadic]/4.21, a declaration of a parameter pack is a pack
expansion. The pack is expanded into a sequence of template arguments
when the deduced template arguments are substituted into it.

[temp.variadic]/4.2:
—  In a template parameter pack that is a pack expansion (14.1):
    — if the template parameter pack is a parameter-declaration; the pattern is the parameter-declaration without the ellipsis;
    — if the template parameter pack is a type-parameter with a template-parameter-list; the pattern is the corresponding type-parameter without the ellipsis.

This implies that some template-parameters are pack expansions. It doesn’t say that every template-parameter with an ellipsis is a pack expansion. Now you seem to be saying that a pack declared such as typename... or int... is intrinsically an expansion of a non-pack pattern to fit an argument list. Earlier, though, you mentioned that such a template-parameter “is ill-formed, as it attempts to expand a non-pack.”

That was based on a misunderstanding.

A declaration of a parameter pack (that is, a template-parameter with an ellipsis before the optional identifier) is a pack expansion if and only if it contains an unexpanded pack. In this case, there is none.

That wording is misplaced; as I recall, there's an existing core issue
on that. As that wording suggests, the pack is deduced to be empty
after the template argument deduction process and prior to
substitution. Substituting T = <whatever> and <anon> = <empty pack>
produces 0 template parameters, substitution into which is vacuous.

There’s no T = <whatever> because substitution into <whatever> already failed.

There's no substitution into <whatever>. <whatever> was deduced from the call. In:

  template<typename T, std::enable_if_t<always_false<T>::value, int>...X>
  void func(T &) {}

  int n;
  func(n);

... T is deduced as 'int', and X is deduced as <empty pack>.

But I think I see what you're saying: X is not notionally expanded into a sequence of parameters in this case, it remains a single parameter whose value is an empty pack, and T should be substituted into the type of that parameter. I think you're right, and this is a Clang bug.

The substitution occurs before deduction when the parameter is explicitly specified.

 template<typename T, std::enable_if_t<always_false<T>::value, int>...>
 void func(T &) {}

 auto q = func<int>; // implementation variance

I suppose the paragraph might be better placed in the next section since it relates to deduction, but that doesn’t affect its meaning.

Right. It's in completely the wrong place; it's part of the rules for deduction, not the rules for substituting explicitly-specified arguments.

Richard Smith

unread,
Feb 3, 2016, 3:44:52 PM2/3/16
to std-dis...@isocpp.org
... fixed in Clang trunk.
Reply all
Reply to author
Forward
0 new messages