Search code examples
c++templateslambdac++17if-constexpr

Dependent name error in discarded if-constexpr instantiation of function template or generic lambda


From my understanding, dependent name lookup doesn't take place until template instantiation, and template invocations in a discarded if-constexpr statement are not instantiated. As such, I would expect a template or generic lambda that is ill-formed, due to missing dependent names, to not produce compilation errors as long as it is only used in discarded if-constexpr statements. This appears to be the case in some instances. For example, take:

struct Struct {};
Struct s;

template<typename T>
void foo(T& s) {
    s.non_existing_member = 0;
}

struct A {
    template<typename T>
    void operator()(T& s) { // note 'void' return type
        s.non_existing_member = 0;
    }
};

struct B {
    template<typename T>
    auto operator()(T& s) { // note 'auto' return type
        s.non_existing_member = 0;
    }
};

As expected, these do not produce compilation errors:

if constexpr (false) {
    foo(s);
    A{}(s);
}

[](auto& s) {
    if constexpr (false) {
        s.non_existing_member = 0;
    }
}(s);

However, these do, complaining about the missing member:

if constexpr (false) {
    auto bar = [](auto& s) {
        s.non_existing_member = 0;
    };

    // error: no member named 'non_existing_member' in 'Struct'
    bar(s); // note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<Struct>'
    B{}(s); // note: in instantiation of function template specialization 'B::operator()<Struct>' requested here
}

I don't quite understand what is different about the above two cases. I get similar errors referencing dependent type names, e.g. typename T::Type.


Rereading the docs (thanks @Jarod42):

Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive.

If a constexpr if statement appears inside a templated entity, and if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated.

I would actually expect foo(s); and A{}(s); to fail compilation as well. They don't, though, with neither of the latest of clang, gcc, or MSVC. I would also expect the following to work, which it does. Full example.

template<typename T>
void baz(T& s) {
    if constexpr (false) {
        s.non_existing_member = 0;
    }
}

struct C {
    template<typename T>
    auto operator()(T& s) {
        if constexpr (false) {
            s.non_existing_member = 0;
        }
    }
};

int main() {
    baz(s);
    C{}(s);
}

Being explicit about the return type of the generic lambda seems to work (allow compilation) with gcc and MSVC, but not with clang:

auto bar = [](auto& s) -> void {
    s.non_existing_member = 0;
};

Solution

  • if constexpr (false) only prevents instantiation of code when there is a surrounding template. Otherwise, it works mostly like if (false).

    The specifics are in [stmt.if] p2:

    During the instantiation of an enclosing templated entity ([temp.pre]), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.

    However, another important paragraph is [basic.def.odr] p12:

    Every program shall contain at least one definition of every function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required.

    As a consequence, odr-use within if constexpr (false) doesn't require instantiation to happen, and in general, templates are only instantiated when needed.

    Case A

    if constexpr (false) {
        foo(s);
        A{}(s);
    }
    

    Here, foo(s) and A{}(s) are located outside any templated entity. However, the calls to foo(s) and A{}(s) are odr-use within a discarded statement, so they don't result in instantiation of those templates.

    Note that foo and A::operator() return void; they don't have deduced return types, so an instantiation isn't needed to check the validity of the program.

    Case B

    if constexpr (false) {
        auto bar = [](auto& s) {
            s.non_existing_member = 0;
        };
    
        // error: no member named 'non_existing_member' in 'Struct'
        bar(s); // note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<Struct>'
        B{}(s); // note: in instantiation of function template specialization 'B::operator()<Struct>' requested here
    }
    

    Once again, if constexpr acts like a simple if because this code is located inside main. Once again, all odr-use within this discarded statement is irrelevant. As stated in [temp.inst] p5, function templates aren't instantiated for no reason:

    Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program.

    A definition isn't required to exist (see [basic.def.odr] p12) and existence of a definition doesn't affect semantics because no constant evaluation takes place (see [temp.inst] p8).

    bar and B::operator() have a deduced return type, and it looks like compilers are using this as an excuse to eagerly instantiate these templates despite not being allowed to. However, that is a bug.

    Case C

    [](auto& s) {
        if constexpr (false) {
              s.non_existing_member = 0;
        }
    }(s);
    
    baz(s);
    C{}(s);
    

    Here, the code inside the discarded statement is not instantiated because there actually is a surrounding templated entity. Namely, the call operator of this generic lambda is implicitly a function template, just like C::operator(). When the call operator is instantiated, s.non_existing_member is not instantiated, so it doesn't cause an error.