c++lambdac++20clang++incomplete-type

Compiler behavior with recursive incomplete types and generic lambda arguments


The following code compiles successfully with GCC 14.1 (-std=c++20) (godbolt), but Clang 18.1.0 (godbolt) fails to compile for some reason:

template <class F>
struct Fix {
    F f;
    using Ptr = decltype(&F::template operator()<Fix<F>>); // member function pointer "void ((lambda)::*)(Fix<(lambda)>) const"
};

int main() {
    auto lambda = []([[maybe_unused]] auto self) -> void {};
    [[maybe_unused]] auto hi = Fix{lambda};
}
fix.cpp:8:44: error: variable has incomplete type 'Fix<(lambda at fix.cpp:8:19)>'
    8 |     auto lambda = []([[maybe_unused]] auto self) -> void {};
      |                                            ^
fix.cpp:4:39: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<Fix<(lambda at fix.cpp:8:19)>>' requested here
    4 |     using Ptr = decltype(&F::template operator()<Fix<F>>); // member function pointer "void ((lambda)::*)(Fix<(lambda)>) const"
      |                                       ^
fix.cpp:9:32: note: in instantiation of template class 'Fix<(lambda at fix.cpp:8:19)>' requested here
    9 |     [[maybe_unused]] auto hi = Fix{lambda};
      |                                ^
fix.cpp:2:8: note: definition of 'Fix<(lambda at fix.cpp:8:19)>' is not complete until the closing '}'
    2 | struct Fix {
      |        ^
1 error generated.

This is a program that demonstrates the concept of fixed-point combinators. The argument auto self is intended to receive Fix<(lambda)>. The type variable Ptr is required to add more complex or practical functionality to the class.

In the definition of Fix, there is a need to instantiate Fix<F> itself, which may be the cause of a compiler error "variable has an incomplete type".

Could someone please tell me the reason why this happens? Is this a specification issue or a bug?

Even more strangely, the following code does compile with Clang:

template <class F>
struct Fix {
    F f;
    using Ptr = decltype(&F::template operator()<Fix<F>>); // member function pointer "void ((lambda)::*)(Fix<(lambda)>) const"
};

int main() {
    auto lambda = []([[maybe_unused]] auto self) -> void {};

    // Added
    using F = decltype(lambda);
    using Ptr = decltype(&F::template operator()<Fix<F>>);

    [[maybe_unused]] auto hi = Fix{lambda};
}

This seems like quite a weird behavior. What's going on here?


Solution

  • I've desugared the lambda and removed unnecessary details. This is what's left:

    template <class F>
    struct Fix {
        using Ptr = decltype(&F::template operator()<Fix<F>>);
    };
    
    struct Lambda {
        constexpr void operator()(auto) const {}
    };
    
    int main() {
        Fix<Lambda>();
    }
    

    As with the original example, GCC accepts it and Clang rejects it. With this desugared form we can observe a fact that might be relevant: Clang starts accepting the code if constexpr is removed.

    This fact suggests that Clang rejecting the code has to do with when the function is needed for constant evaluation. Under [temp.inst]/8, when a function becomes needed for constant evaluation, its definition is implicitly instantiated. Under [expr.const]/21.6, this specialization of Lambda::operator() is needed for constant evaluation because it is a constexpr function and is named by the expression &F::template operator()<Fix<F>>, which is potentially constant evaluated under [expr.const]/21.4 because it is an address-of expression that occurs within a templated entity (class template Fix). Upon instantiation of Lambda::operator()'s definition, it becomes subject to [dcl.fct.def.general]/2, which prohibits incomplete types as parameter types or return types of function definitions.

    This instantiation doesn't actually take place until we get to main, at which point the definition of the template Fix has been seen already. The argument for the code being ill-formed must be that Fix<Lambda> is still incomplete when Lambda::operator()<Fix<Lambda>> is instantiated because the former is still in the process of being instantiated (even though the template has been completely defined already). The standard appears to give Fix<Lambda> the same point of instantiation as Lambda::operator()<Fix<Lambda>>, so it doesn't seem to give an unambiguous answer to this question. In my opinion Clang's behavior seems like what I would expect, based on the idea that the compiler is "in the middle" of generating the concrete instantiation Fix<Lambda> by instantiating the member declarations one-by-one from the primary template, and thus hasn't completed Fix<Lambda> yet.

    I'm not sure how GCC manages to accept it. It may be that GCC has still not fully implemented P0859R0. If we do something that really forces GCC to instantiate, then it does admit that Fix<F> is incomplete:

    template <class F>
    struct Fix {
        int x = 1;
        using A = int[F::template operator()<Fix<F>>()];
    };
    
    struct Lambda {
        template <class T>
        static constexpr int operator()() {
            return T().x;
        }
    };
    
    int main() {
        Fix<Lambda>();
    }