Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Will C++ lambda really make copies of parameters captured by copy?

Using a lambda function in C++ with variables captured by value implies a copy of the value.

With a good compiler, and assuming we do not modify the value within the lambda function, can we hope that there will be no actual copy once the code is compiled and optimized?

For example, in the following it seems to make sense to pass new_item as a value since it is used in read only mode.

void loadavg_file::add(loadavg_item const & new_item)
{
    auto const & it(std::find_if(
            f_items.begin(),
            f_items.end(),
            [new_item](auto const & item)
            {
                return (item.f_address == new_item.f_address);
            }));

    if(it == f_items.end())
    {
        f_items.push_back(it);
    }
    else
    {
        // replace existing item with new avg and timestamp
        it->f_timestamp = new_item.f_timestamp;
        it->f_avg = new_item.f_avg;
    }
}

Will the loop be optimized out and result in absolutely no copy of new_item?

like image 427
Alexis Wilke Avatar asked Feb 06 '23 09:02

Alexis Wilke


2 Answers

If the copy constructor of new_item (i.e. loadavg_item::loadavg_item(loadavg_item const&)) has observable effects other than allocation of memory, then those effects must be observed to occur (as long as, y'know, you actually make an effort to observe them).

This is because you might be depending on those side effects occurring, and this is not a context where copy elision is allowed; copy elision is only allowed (and, latterly, mandated) when returning a value from a function. On the other hand memory allocation elision is allowed anywhere (subject to the rules in [expr.new]/10); clang is particularly good at this.

Inspecting the generated assembly does not count as observation of side effects, and nor does running the program in a debugger.

If the copy constructor of new_item is non-inline, then the assembly of the translation unit might exhibit a call to the copy constructor as a symbol, but link-time optimization (LTO) can still elide that call if the link-time optimizer can deduce that the copy constructor has no observable side effects.

like image 54
ecatmur Avatar answered Feb 24 '23 12:02

ecatmur


Yes, a copy is two copies (because the lambda is passed by value, its members get copied again) are required for your code.

If the entire call tree (find_if, copy constructor, destructor, operator==, any functions those call) consists of visible functions and the compiler choose to inline them, it is possible that further optimizations such as common-subexpression-elimination may reduce or eliminate the runtime cost of those copies.

In the process, the compiler will have to prove that

  • The value of the copy doesn't change (because such changes should not propagate to the original)
  • The copy constructor has no side effects.
  • The destructor has no side effects.
  • None of the functions rely on the object's identity.
  • The copy's lifetime cannot exceed the lifetime of the object referred to by the parameter.
  • No other aliases exist for the object referred to by the parameter, or such other aliases will not be used to modify said object while the lambda is in use, including accesses from other threads (if synchronized).

If you want to avoid the copy, don't write code asking for that copy. Or copy only the needed portion of the object, e.g.

auto const & it(std::find_if(
        f_items.begin(),
        f_items.end(),
        [key = new_item.f_address](auto const & item)
        {
            return (item.f_address == key);
        }));
like image 30
Ben Voigt Avatar answered Feb 24 '23 12:02

Ben Voigt