lambda function with incomplete type
Asked Answered
W

1

6

The following code compiles fine:

#include <cstddef>

struct A {
    char a;
    static constexpr int off(void) { return offsetof(A, a); }
    static constexpr int (*off_p)(void) = off;
};

The following seemingly similar code that just uses lambda for short, doesn't compile:

#include <cstddef>

struct A {
    char a;
    static constexpr int (*off_p)(void) =
         [](void) static constexpr ->int { return offsetof(A, a); };
};
$ g++ -std=c++23 bad.cpp 
In file included from /usr/include/c++/13/cstddef:50,
                 from bad.cpp:1:
bad.cpp: In static member function ‘A::<lambda()> static’:
bad.cpp:5:74: error: invalid use of incomplete type ‘struct A’

So basically I have 2 separate questions, since I don't understand what's going on here.

  1. Why in the first case the use of incomplete type is allowed?
  2. Why in the second case the use of incomplete type is NOT allowed?

Compiler explorer demo.

Weidman answered 22/10, 2023 at 18:40 Comment(19)
Can you even use a lambda in a constexpr context? A lambda is not a function, it is a class type. The compiler has to create an object instance of the lambda, and then invoke a conversion operator on it to obtain a function pointer from it.Cosmorama
The static keyword inside your lambda upsets my compiler (MSVC) but, without that, I get the same error that you report. Is that use of static a g++ thing?Vanscoy
static or constexpr lambdas are c++23, so you need to change the language standard in MSVC.Weidman
@Weidman I am using the "latest" Standard in MSVC (which is C++23, IIRC). But I also note that its definition of offsetof uses a reinterpret_cast, which can screw up constexpr stuff.Vanscoy
You can try clang++ then. It uses __builtin_offsetof() instead. Or for example you can use sizeof(A) instead of offsetof.Weidman
@Weidman Hmm. Seems like MSVC also uses the built-in version (but Intelli-non-Sense doesn't). I get this warning: message : offsetof has a builtin meaning; use /Zc:offsetof- to revert to old, non-conforming definitionVanscoy
Hmm, Ted deleted his answer for some unclear reasons... So you need to repeat what you said there, as I no longer see the replies. What did you mean by lambda being a data member? I am not sure I understand.Weidman
@Weidman I had to go and couldn't stay to keep updating my answer. I may pick it up later if I have time and noone else has given an answer.Trident
@TedLyngmo thanks, although of course the main thing is to clarify what is it a `static constexpr lambda". You say its not a member function, but the conclusive answer needs to say what is it then.Weidman
The lambda is an instance of an anonymous class type, unrelated to A.Trident
But if it is unrelated to A, then, just as with member function moved outside of A, it should see A as a complete type?Weidman
@Weidman The lambda is defined where you write it. The member function is defined after the class definition is complete.Trident
@TedLyngmo But its static! How can something static be defined where I write it, i.e. directly in a class? For example static data members are AFAIK also moved outside of a class def, no? I don't think something that is static can be created (and allocate some space?) directly when I write it, i.e. in this class.Weidman
@Weidman Yes, the call operator is static. With a normal class it would look like this: godbolt.org/z/drvj3fhv1Trident
But your example on godbolt compiles fine, so what is the point of it? Its very similar to what lambda should do, but it does compile, and lambda not. In any case, I already accepted another answer.Weidman
@Weidman The point was to show what static applies to.Trident
You showed, but it compiled. :) Hence my question "why lambda doesn't" wasn't solved at all. I had to choose another answer, I hope you see what I mean by saying that its really a conclusive one now. It doesn't leave a single place to any "oh but why this, oh but what that".Weidman
@Weidman Yes, I showed a similar construct using a normal nested class only to show what static applies to since you wondered about it. Just disregard my example and focus on "Yes, the call operator is static" - as in, the lambda's call operator.Trident
OK, I see, your point is that static applies only to operator() and therefore is irrelevant to whether or not this is a point of "complete-class context" or not. Thanks for that detail.Weidman
S
6

There's a finite list of places inside the member specification of a class (i.e. before the closing brace), where it is considered complete:

[class.mem.general]

7 A complete-class context of a class (template) is a

  • function body ([dcl.fct.def.general]),
  • default argument ([dcl.fct.default]),
  • default template argument ([temp.param]),
  • noexcept-specifier ([except.spec]), or
  • default member initializer

within the member-specification of the class or class template.

8 A class C is complete at a program point P if the definition of C is reachable from P ([module.reach]) or if P is in a complete-class context of C. Otherwise, C is incomplete at P.

Now, while we may be tempted to think that a function body or default member initializer applies to the lambda case, they in fact do not. To start with, default member initializers appear in non-static members only (that's just how the standard terminology is laid out, static member initializers are defined as something else).

And as for "function body", note that the bullet refers to a relevant section where the grammar production of a function body is defined:

function-body:
  ctor-initializeropt compound-statement
  function-try-block
  = default ;
  = delete ;

On the other hand, the grammar for a lambda expression doesn't reuse the function-body grammar at all:

lambda-expression:
  lambda-introducer attribute-specifier-seqopt lambda-declarator compound-statement
  lambda-introducer < template-parameter-list > requires-clauseopt attribute-specifier-seqopt lambda-declarator compound-statement

So with a lambda, we are in fact not in a complete class context, so paragraph 8 forces the compiler to treat A as incomplete.

Stace answered 22/10, 2023 at 20:42 Comment(7)
Wow! I was about to write "But hey, I can remove static constexpr from off_p, and, according to your answer, it will then fall into the default member initializer category!". To my biggest surprise, I've found that then it actually compiles! So your answer is definitely the right one. But don't you think it is a bit silly to allow that case only for non-static members? I am very surprised by that finding, to say the least.Weidman
@Weidman - I don't think it's intentional. There's a fair bit of historical baggage in C++, and sometimes new features stumble at prohibitions that prexisted them. Since the design is by commitee, someone has to notice and champion these bits of evolution as they come up, which doesn't always happen.Stace
But C++23 is not yet released, so maybe its not too late to tell them so? :)Weidman
@stsp: C++23 was finished in February, but the issue even for C++26 is that lambda expressions can be called where they are created, so they don’t fit the “deferred parsing” model well.Horwath
@DavisHerring timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#3 defined "closure type" as a regular class. A "complete-class context of a class" includes eel.is/c++draft/class.mem.general#7.2 "deafult argument". And there is a "Note 4" that says: "A complete-class context of a nested class is also a complete-class context of any enclosing class". To me that means that offsetof() put as a default argument of a lambda, should be a "complete-class context" of an enclosing class, and as such, the "deferred parsing" should still work. In practice it doesn't though. :(Weidman
@stsp: Default arguments being complete class contexts is a bit broken.Horwath
@StoryTeller-UnslanderMonica: There is a reason that this is allowed for non-static members only: the initializer for a static member can affect layout (via its deduced type or via its usable-in-constant-expressions value), but that for a non-static member cannot.Horwath

© 2022 - 2024 — McMap. All rights reserved.