c++templatesc++14language-lawyer

Does the standard specify when function template instantiation side-effects are visible?


Note that I am not asking "When do function templates get instantiated?" Rather, "When are the side-effects of that instantiation visible?"

An example of a side-effect of an instantiation would be friend injection. For instance:

// Same result on Clang, GCC, and MSVC.

template<int N>
struct FriendIndex {
    friend constexpr bool Get(FriendIndex);
};

template<int N, bool val>
struct FriendInjector {
    friend constexpr bool Get(FriendIndex<N>) {
        return true;
    }
};

template <bool val>
auto InstantSideEffect() {
    (void)FriendInjector<0, val>{};
    return 0;
}

template <bool val>
int DelayedSideEffect() {
    (void)FriendInjector<1, val>{};
    return 0;
}

int dummy0 = InstantSideEffect<true>();
// Will succeed.
static_assert(Get(FriendIndex<0>{}),"");

int dummy1 = DelayedSideEffect<true>();
// Will fail: Get(FriendIndex<1>) is not a constant expression.
static_assert(Get(FriendIndex<1>{}),"");

By instantiating InstantSideEffect<true>(), the constexpr friend function Get(FriendIndex<0>) is defined right after instantiation, and can be used in static_assert.

On the other hand, by instantiating DelayedSideEffect<true>(), the definition of Get(FriendIndex<1>) is not visible right after instantiation, and static_assert using it will fail to compile.

Since the only difference between the two functions is that InstantSideEffect() returns auto and DelayedSideEffect() returns int, I looked at the standard to see if it had any info about auto affecting when these side-effects are visible.

The draft C++14 standard, says in 7.1.6.4 [dcl.spec.auto]/12:

Return type deduction for a function template with a placeholder in its declared type occurs when the definition is instantiated even if the function body contains a return statement with a non-type-dependent operand. [ Note: Therefore, any use of a specialization of the function template will cause an implicit instantiation. Any errors that arise from this instantiation are not in the immediate context of the function type and can result in the program being ill-formed. — end note ]

This makes it sound like auto affects instatiations for specialized function templates, and that auto possibly affects SFINAE. However, I don't see anything in the standard specifying when side-effects are visible.

Is there some part of the standard that touches on when side-effects like friend injection are visible? Or is it just a coincidence that the three big compilers make friend injections instantly visible for auto function templates, and that could change at any time?


Solution

  • First of all, the standard does not distinguish between int and auto return types in a way that is relevant to your example. See [temp.inst]/3:

    Unless a function template specialization has been explicitly instantiated or explicitly specialized, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist. Unless a call is to a function template explicit specialization or to a member function of an explicitly specialized class template, a default argument for a function template or a member function of a class template is implicitly instantiated when the function is called in a context that requires the value of the default argument.

    When you actually call a function template specialization, the definition must exist. That definition is not necessarily required to be present in the same translation unit; it might be explicitly instantiated in a different translation unit. But, in cases where the definition is present in the same translation unit, a call triggers the implicit instantiation of the definition. So, in your case, both InstantSideEffect and DelayedSideEffect are instantiated when called, and that means the corresponding FriendInjector specializations are instantiated along with them.

    However, implementations have latitude to defer instantiation of function template specializations until a later point in the translation unit under [temp.point]/8:

    A specialization for a function template, a member function template, or of a member function or static data member of a class template may have multiple points of instantiations within a translation unit, and in addition to the points of instantiation described above, for any such specialization that has a point of instantiation within the translation unit, the end of the translation unit is also considered a point of instantiation. A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. If two different points of instantiation give a template specialization different meanings according to the one definition rule ([basic.def.odr]), the program is ill-formed, no diagnostic required.

    The last sentence basically means that the house always wins. If you wrote code that depends on the point of instantiation being in one place and the implementation decides to put it somewhere else, then the implementation will reject your code or your code will behave differently than expected, which it is allowed to do because the program is ill-formed, no diagnostic required. If the implementation decided to put the point of instantiation where you were hoping, then the implementation will accept your code, which it is also allowed to do.

    It seems that the implementations you've tested all use the strategy of deferring instantiation of DelayedSideEffect until the end of the translation unit, which means it is too late for Get(FriendIndex<1>{}) to be a constant expression at the point where the latter appears. But your program is IFNDR because its meaning changes depending on whether the instantiation is deferred.

    On the other hand, templated functions that have a deduced return type will not have their instantiation deferred because the compiler needs to know the type of the function before it can continue semantic analysis.

    Final note: The wording in the standard is not exactly right. It defines "point of instantiation", but the only normative meaning given to points of instantiation is that they control name lookup. Whether or not a function definition has been instantiated yet is not a name lookup issue. The last sentence of [temp.point]/8 means to give implementations broad latitude to defer instantiation, not only with respect to name lookup, so the wording does not quite reflect the intent.