19

Consider two structs with different member type aliases:

struct foo { using x = int;   };
struct bar { using y = float; };

Given a T in a template context, I want to get either T::x or T::y depending on what T is:

template <typename T>
auto s()
{
    auto l = [](auto p) 
    {
        if constexpr(p) { return typename T::x{}; }
        else            { return typename T::y{}; }
    };

    return l(std::is_same<T, foo>{});
}

int main() 
{ 
    s<foo>(); 
}

g++ compiles the code above, while clang++ produces this error:

error: no type named 'y' in 'foo'
        else            { return typename T::y{}; }
                                 ~~~~~~~~~~~~^
note: in instantiation of function template specialization 's<foo>' requested here
    s<foo>();
    ^

on godbolt.org, with conformance viewer


Is clang++ incorrectly rejecting this code?

Note that clang++ accepts the code when removing the indirection through the generic lambda l:

template <typename T>
auto s()
{
    if constexpr(std::is_same<T, foo>{}) { return typename T::x{}; }
    else                                 { return typename T::y{}; }
}
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416

1 Answers1

6

See Richard Smith's post on std-discussion:

In the implementation I'm familiar with [i.e. Clang], a key problem is that the lexical scopes used while processing a function definition are fundamentally transient, which means that delaying instantiation of some portion of a function template definition is hard to support. Generic lambdas don't suffer from a problem here, because the body of the generic lambda is instantiated with the enclosing function template, [..]

That is, generic lambdas' bodies are partially instantiated using the local context (including template arguments) when the template is instantiated; thus under Clang's implementation, T::x and T::y are substituted directly, since the closure type could be passed outside. This leads to the failure. As pointed out by @T.C., the code can be considered ill-formed, no diagnostic required, as the instantiation of s<foo> yields a template definition (that of the closure) whose second if constexpr branch has no well-formed instantiations. This explains the behaviour of both Clang and GCC.

This boils down to an architectural issue in a major implementation (see also this answer; GCC apparently doesn't suffer from this limitation), so I'd be surprised if Core would deem your code well-formed (after all, they accounted for this in the design of generic lambda captures--see the linked answer). GCC supporting your code is, at best, a feature (but probably harmful, since it enables you to write implementation-dependent code).

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • 1
    What is the corresponding standardise that rules this illformed? I thought that discarded statements are not checked? – Johannes Schaub - litb Jun 30 '17 at 11:41
  • @JohannesSchaub-litb I believe there is none. Discarded statements are only discarded statements in the context of the instantiation of their template. In the surrounding context, they're just code. – Columbo Jun 30 '17 at 12:31
  • Hmm, I'd go further. After substituting in `T`, one of the branches of the resulting `if constexpr` has no valid specialization, so it's ill-formed NDR by [temp.res]/8. This is probably not intended - see the introduction in P0588R0. – T.C. Jun 30 '17 at 16:21
  • @T.C. Huh. I didn't know that substatements need to have a valid specialization. – Columbo Jun 30 '17 at 16:24
  • @T.C. Not sure that argument applies. After all, we're talking about a nested template; there must be a valid specialization over the template arguments of all enclosing templates, and we can certainly provide that. – Columbo Jun 30 '17 at 16:28
  • Yes, the argument is that the instantiation of `s` yields, among other things, a template that itself needs to follow [temp.res]/8. – T.C. Jun 30 '17 at 16:35
  • Well, it by its words applies to all "templates", generated from instantiation or not. Of course, this situation is rather unique... – T.C. Jun 30 '17 at 16:38
  • @T.C. That interpretation would break tons of code. E.g. [this](http://coliru.stacked-crooked.com/a/9e8ef4584deb818b) would be ill-formed NDR, because there's some member function `f` that operates on objects of type `T`? – Columbo Jun 30 '17 at 16:50
  • 1
    @T.C. that only applies if the template definition is actually instantiated as a template. Is it here? – Johannes Schaub - litb Jun 30 '17 at 17:25
  • When I reported that "issue" to the committee, they told me that it is not intended that template definitions are ever instantiated as templates. As a result, they fixed explicit instantiation to exclude member templates from being instantiated as templates. – Johannes Schaub - litb Jun 30 '17 at 17:30
  • However in this case of a generic lambda, I agree it looks as though that the lambda body is instantiated as a template, as there is no ondemand instantiation – Johannes Schaub - litb Jun 30 '17 at 17:33
  • 1
    @columbo it does not apply to that, because the member template definition is not instantiated, but only the declaration parts. – Johannes Schaub - litb Jun 30 '17 at 17:37
  • @JohannesSchaub-litb Where did you report this? Is this a dr? – Columbo Jun 30 '17 at 17:52
  • 1
    @Columbo What Johannes said. The body of `f` isn't instantiated, so the only "template" produced by the instantiation is a declaration of `f`, which is perfectly well-formed. Re the report, I think he's talking about CWG 1532. – T.C. Jun 30 '17 at 18:29
  • Yes, cwg1532. @t.c. however if you call the member function template in his example, then in order to instantiate the definition, it has to first instantiate the template definition(?). So then the definition exists and causes illformed, NDR (UB). Or am I missing something? Prolly it doesn't matter because it would render an illformed program NDR, but still.. – Johannes Schaub - litb Jun 30 '17 at 18:40
  • @JohannesSchaub-litb In that case [temp.res]/8.1's other condition is not satisfied ("and the template is not instantiated"). – T.C. Jun 30 '17 at 18:42
  • @t.c. I don't see yet why the lambda in this question is illformed NDR tho. There are template parameters for operator() that yield valid specializations of the polymorphic lambda. – Johannes Schaub - litb Jun 30 '17 at 19:02
  • @JohannesSchaub-litb each branch of the `if constexpr` needs to be well-formed for some template parameters of the `operator()`. The second branch is never well-formed. – T.C. Jun 30 '17 at 19:03
  • @t.c. what rule in the spec says that? Does this mean that `if constexpr(false) void t;` is illformed as well? That would seem to reduce the usability of if constexpr dramatically. – Johannes Schaub - litb Jun 30 '17 at 20:51
  • @JohannesSchaub-litb The same rule in [temp.res]/8.1. And yes. – T.C. Jun 30 '17 at 20:51
  • @T.C. Ah I see. But I think the second answer should be "it depends", because if the if constexpr is not within a template, then the paragraph doesn't apply. Neither does it apply if the template is instantiated. So I think it won't apply to the question, because the lambda is called (which is kind of weird, I think). – Johannes Schaub - litb Jun 30 '17 at 21:03
  • @JohannesSchaub-litb Discarded statements are fully checked outside of templates, so that argument just made it worse. See also https://timsong-cpp.github.io/cppwp/stmt.if#2.sentence-3. – T.C. Jun 30 '17 at 21:06
  • @T.C. wow, I had no idea, thanks (they aren't fully checked tho. Things referenced within discarded statements do not need to be defined). – Johannes Schaub - litb Jul 01 '17 at 09:48