Is it mandatory to infer the type of the default template argument on each instantiation?
Asked Answered
L

1

7

Having this code, GCC and MSVC are both happy with it, when clang complains. Any two lambdas must have different types, which makes me think that clang "caches" the type of the default template argument (but only if the class itself is a template).

UPDATE: What I mean here is I expect that every time I instantiate the function template it should infer the type of the default argument. But what truly happens (in the third test) is clang instantiate the function template only once (on the first use). This is is inconsistent not olny with both other major compilers but even with clang itself. As you can see such a behavior only applies if the function template defined/declared in a class template (doesn't matter if class/function template arguments are dependent or not). In other two tests (free function and method in non-class-template) the type is inferencing on each instantiation.

Is there any formal rule for that? Or it's undefined/unspecified/implementation specific behavior?

#include <type_traits>

template <typename T = decltype([](){})>
T f();

struct Test1 {
    template <typename T = decltype([](){})>
    T f();
};

template <typename = void>
struct Test2 {
    template <typename T = decltype([](){})>
    T f();
};

int main()
{
    static_assert(!std::is_same_v<decltype(f()), decltype(f())>);

    Test1 test1;
    static_assert(!std::is_same_v<decltype(test1.f()), decltype(test1.f())>);

    Test2 test2;
    static_assert(!std::is_same_v<decltype(test2.f()), decltype(test2.f())>); // GCC and MSVC ok with this line, clang complains
}

live example

Linotype answered 6/9, 2024 at 19:6 Comment(8)
I don't know what "caching" you are referring to. Each of the lambdas [](){} is unique, per [expr.prim.lambda.closure]: "The type of a lambda-expression is a unique, unnamed non-union class type." Each occurrence of[](){} is a distinct type, and clang appears to be correct. It's as if you had written struct L1 { void operator()(){}}; struct L2 { void operator()(){}}; struct L3 { void operator()(){}}; and then used template<typename T = L1> for f(), template<typename T = L2> for Test1::f(), and template<typename T = L3> for Test2::f().Infertile
@RaymondChen Well, is it the same as that? Clang's behavior is not internally consistent, since it thinks that f() evaluates as f<decltype([]{})>() (i.e. every call evaluates with a unique lambda) but test2.f() evaluates as test2.f<L3>() (i.e. every call evaluates with the same lambda). The other two compilers evaluate the default argument on the call-site in every case (consistently).Babirusa
f() and f<decltype([]{})>() are not the same thing. The first uses the lambda declared on line 3. The second uses the lambda declared inline. The way to understand this is to replace all lambdas with explicitly named types. Create a new explicitly named type for each occurrence of a lambda in the source code.Infertile
@RaymondChen The question points out that neither gcc nor clang nor msvc evaluate f() in the way you are describing.Babirusa
@Babirusa Right, and my reading is that clang is following the standard and the others aren't.Infertile
@RaymondChen I think Clang is wrong regardless. Either it is incorrect to reject the last assertion, or it is incorrect to accept the first two. It's just a matter of which side is correct.Babirusa
Oh, I misread the third test. I though the third test was std::is_same_v<decltype(test1.f()), decltype(test2.f())>). My read of the standard is that all three assertions should fail (since the first is asserting that L1!=L1, the second is asserting that L2!=L2, and the third is asserting that L3!=L3. But it seems all three major compilers disagree with me, so I must be misreading something.)Infertile
Consider the case when the template is in a header file, and you have two translation units with that lambda default. Do the two translation units instantiations with the defaulted type have the same type? What do the "big three" (clang, gcc, msvc) generate in that case? Should they have the same type?Hyphenate
L
5

This is unfortunately just a gap in the specification. The standard doesn't explain how any of these 3 examples should behave. [expr.prim.lambda.closure]/1 says:

The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below.

The meaning of "unique" has never been elaborated. Nor does the specification of default template arguments explain anything useful here.

Because a default template argument can reference earlier template arguments in the same template parameter list, it would appear that a default template argument may have to be instantiated multiple times for the same template (at least, when different values are supplied to the preceding template arguments). From there it's not too much of a leap to conclude that perhaps a default template argument is re-instantiated every time it's used, and if it contains a lambda then you get a different lambda every time (and if it doesn't contain a lambda then you just get what you get). But again, the standard doesn't explicitly spell this out.

Lavonia answered 7/9, 2024 at 3:19 Comment(2)
I marked this question as answered, because I believe the answer is "the standard says nothing about it". However, it's more general then just lambdas. The question is at which point the compiler should resolve a default template parameters. Is it mandatory to resolve the type on each instantiation or the compiler is allowed to "save" the function signature on first instantiation and then use it everywhere w/o "reevaluating" the type?Linotype
@Linotype Please rephrase your question in terms of observable behaviour of a program. Otherwise I don't know what you mean by "reevaluating" a type.Lavonia

© 2022 - 2025 — McMap. All rights reserved.