Some important pieces of this code:
- A lambda captures a
DynamicValue
object by value (copy).
- The lambda is used to initialize a
std::variant
as a std::function
alternative.
- There is no explicit move constructor for
DynamicValue
, so the template for invocable objects is used as the move constructor.
The problematic code path starts with the request to construct a DynamicValue
object from the lambda. This invokes the template constructor, which attempts to copy the lambda into the std::function
alternative of the variant
. So far, so good. Copying (not moving) the lambda copies the captured object without problems.
However, this procedure works when the CopyConstructible named requirement is satisfied. Part of this named requirement is being MoveConstructible. In order for a lambda to satisfy MoveConstructible, all of its captures have to satisfy that named requirement. Is this the case for DynamicValue
? What happens when your standard library tries to move the lambda (hence also the captured object), with copying as the fallback? While DynamicValue
has no explicit move constructor, it is invocable...
When F
is DynamicValue<T>
, the template constructor serves as the move constructor. It tries to initialize the variant
by converting the source DynamicValue
(the captured copy of a
in the question's code) into a std::function
. This is allowed, a copy of the source is made, and the process continues until the copy needs to be moved, at which point the move constructor is again invoked. This time, it tries to initialize the variant
by converting the copy of the source DynamicValue
into a std::function
. This is allowed, a copy of the copy of the source is made, and the process continues until the copy of the copy needs to be moved, at which point the move constructor is again invoked. Etc.
Instead of moving the DynamicValue
into the new object, each "move constructor" tries to move the DynamicValue
into the variant
of the new object. This would add another layer of overhead with each move, except the recursive calls blow up before construction finishes.
The solution is to make DynamicValue
move constructible. There are at least two ways to do this.
1) Explicitly provide a move constructor.
DynamicValue(DynamicValue&& value) : getter(std::move(value.getter)) {}
2) Exclude DynamicValue
from being a template argument to the template constructor.
template <typename F, typename = std::enable_if_t<std::is_invocable_v<F>>,
typename = std::enable_if_t<!std::is_same_v<std::decay_t<F>, DynamicValue>>>
DynamicValue(F&& function) : getter(function) {}
Note that this excludes only DynamicValue<T>
from being a template argument, not DynamicValue<U>
when U
is not T
. That might be another issue to contemplate.
You might want to see if this also fixes whatever problem led you to define a second copy constructor. That may have been a band-aid approach that did not address this underlying issue.