5

In the following, one way of initialising a member is accepted and the other is an error. I don't understand:

template <typename T>
struct LambdaHolder
{
    LambdaHolder(T lambdaIn) { lambda = lambdaIn; } // This will not work
    LambdaHolder(T lambdaIn) : lambda(lambdaIn) { } // This will work fine
    T lambda;
};


int main()
{
    auto lambda = []() { return 0; };
    LambdaHolder<decltype(lambda) > foo(lambda); // This will work only if the constructor argument is assigned in the initialiser list

// On the other hand
    int(*lambdaPtr)() = lambda; // A function pointer
    LambdaHolder<decltype(lambdaPtr)> foo(lambdaPtr); // Will work either in initialiser list or constructor body

}

I'm not understanding a couple of things. I know that certain members must be initialised in the member initialiser list, like references and const members. What I don't understand is why it will let me copy the lambda in the initialiser list but not in the constructor body. The error is "you cannot construct a copy of a lambda".

Also, I was able to create a pointer to the lambda, and initialising the pointer in either the initialiser list or the constructor body worked. Now, I know that lambdas and function pointers aren't exactly the same type, but the thing is that I thought that a captureless lambda is implemented as a free function, which is why you can set a normal function pointer to it, and also thought that when passing as an argument it would definitely decay to a pointer.

Basically I would like some clarification this, what's the difference for the distinction between initialising in the constructor body or initialiser list?

Edit: It seems, as Whoan has pointed out, that a lambda's default constructor's deleted before C++14, and non-existent after C++14 I think.

Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • Really? What compiler gives the error message "you cannot construct a copy of a lambda"? – aschepler Sep 26 '17 at 01:54
  • Visual Studio, should it now? Oh I should try some online compilers – Zebrafish Sep 26 '17 at 01:56
  • How do I make my compiler give the error message you said? I use `g++ -std=c++14`. – iBug Sep 26 '17 at 01:56
  • On Visual Studio it's C3497 "You cannot construct a copy of a lambda." I'm not sure the Windows compiler flags are the same. I'd better check this out on some online IDEs. The code as posted should be pastable into a compiler... – Zebrafish Sep 26 '17 at 01:59
  • Hmm, MSVC compiler 19.00.23506 gives me "error C3497: you cannot construct an instance of a lambda". No mention of "copy". – aschepler Sep 26 '17 at 02:23
  • "Instance" is the word, yeah, sorry. The GCC compiler online ran off all the errors, deleted default constructor, deleted assignment operator, deleted everything. – Zebrafish Sep 26 '17 at 02:27

2 Answers2

4

That is because lambdas are not Default Constructible:

Closure types are not DefaultConstructible. Closure types have "a deleted (until C++14) | no (since C++14)" default constructor.

In this constructor:

LambdaHolder(T lambdaIn) { lambda = lambdaIn; } // This will not work

...lambda needs to have been default constructed before being assigned.

whoan
  • 8,143
  • 4
  • 39
  • 48
  • Thanks for that, the little subleties always get me. Visual Studio was unhelpful, when I tried online it gave me errors default constructor, assignment operators are deleted. – Zebrafish Sep 26 '17 at 02:11
3

First off, there's a difference between initialization and assignment, although they sometimes look similar.

SomeClass obj1 = val;

is an initialization. It attempts to pass the expression val to a constructor of SomeClass in order to create obj1. Note even if you've defined one or more operator= for SomeClass, none of them will be called by the above code.

SomeClass obj2;
obj2 = val;

In this example, the first statement still needs to initialize obj2 to create a valid object that can be used, so it requires calling a default constructor of SomeClass (either defined by you or automatically by the compiler). Then the second statement assigns to the already initialized object obj2, using an operator= function (also either defined by you or by the compiler).

Whenever a constructor is called, all base classes and members must be initialized before the constructor body starts. The body might try to do anything to members, so they need to be valid initialized objects by that point.

LambdaHolder(T lambdaIn) : lambda(lambdaIn) { }

For any class type T, this constructor initializes the member called lambda using the argument lambdaIn. Since they have the same type, that means calling a copy constructor. This one is perfectly valid.

LambdaHolder(T lambdaIn) { lambda = lambdaIn; }

For any class type T, since the mem-initializer for member lambda is missing but the member must be initialized before the body starts, the compiler attempts to call a default constructor for T to create lambda. Then the statement in the body lambda = lambdaIn; tries to call an operator= function to assign the member from the argument.

Yes, the type of a lambda is a class type. A lambda is essentially a shortcut for making a class whose single most useful member is a function named operator(), so you can call it like a function.

But lambda types have deleted default constructors and deleted copy assignment operators. This means you're never allowed to call them, so the above code is invalid in two different ways.

And by the way, lambda expressions never automatically "decay" into pointers to functions in the sense that C-style arrays decay into pointers to the first element and names of functions decay into pointers to functions. That type of decay only happens to raw array and function types. Instead, there's an implicit conversion from the type of a lambda with no capture list to a function pointer. The implicit conversion only gets used when you actually try to initialize or assign a specific matching function pointer type from the lambda.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Thanks for clearing that up. Lambdas don't decay, they're implicitly converted to a pointer of correct type. Captureless ones at least. – Zebrafish Sep 26 '17 at 02:24
  • 1
    I'm just wondering, if there's no default constructor, and the lambda doesn't capture anything, how is it constructed? How do you construct something with no default constructor when there's nothing arguments to pass because the object is empty (members-wise)? I'm confused – Zebrafish Sep 26 '17 at 02:35
  • @Zebrafish We can't default-initialize them in the ordinary ways, but the compiler is allowed to just produce an object to represent the original lambda expression we typed. In fact, since there are no data members needed, all the compiled code probably has to do is allocate one byte, not caring about its value, and claim that's the lambda object. – aschepler Sep 26 '17 at 02:42