Lambdas as const ref

Author: Chloé Lourseyre

Context

This week, I will present you a piece of code I bumped into not so long ago.

It looked like that:

#include <vector>
#include <algorithm>

int count_even(const std::vector<int>& v)
{
    const auto& my_func = [] (int i)->bool
    {
        return i%2==0; 
    };

    return std::count_if(std::cbegin(v), std::cend(v), my_func);
}

Seeing this, VisualStudio was unsure if it should compile or not.

And by unsure, I mean that a compilation error stating something like “my_func is used but it is already destroyed” was periodicly prompted then discarded during the compilation of the library. But in the end, it compiled.

When I saw this, I thought two things:

“Wait, we can bind a temporary to a const ref?”

and

“What’s the use of binding a lambda to a const ref?”

These are the questions I will answer today.

Binding a rvalue to a const reference

To be short: yes, you can bind a rvalue to a const ref.

Intuitivly, I would have said that trying to do so will only result in a dangling reference, but it will not.

This language is smart, I tend to forget it, but it is only logical that if you try to bind a cons ref to a temporary object, the object will be put on the memory stack and accessed via the const ref.

To illustrate this, here are two functions:

void foo()
{
    const int& i = 1;
    const int& j = i+1;
}

void bar()
{
    int iv = 1;
    const int& i = iv;
    int jv = i+1;
    const int& j = jv;
}

The function foo direclty binds the rvalues to const refs, as the function bar instanciate const values before binding them to cons refs (thus binding a lvalue to a const ref).

The assembly code that clang generate for this piece of code is the following:

foo():                                # @foo()
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 12], 1
        lea     rax, [rbp - 12]
        mov     qword ptr [rbp - 8], rax
        mov     rax, qword ptr [rbp - 8]
        mov     eax, dword ptr [rax]
        add     eax, 1
        mov     dword ptr [rbp - 28], eax
        lea     rax, [rbp - 28]
        mov     qword ptr [rbp - 24], rax
        pop     rbp
        ret
bar():                                # @bar()
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 4], 1
        lea     rax, [rbp - 4]
        mov     qword ptr [rbp - 16], rax
        mov     rax, qword ptr [rbp - 16]
        mov     eax, dword ptr [rax]
        add     eax, 1
        mov     dword ptr [rbp - 20], eax
        lea     rax, [rbp - 20]
        mov     qword ptr [rbp - 32], rax
        pop     rbp
        ret

These two functions are (except from the stack alignment details, which are not really important) the same.

This fact is confirmed by the IBM documentation: Initialization of references (C++ only) – IBM Documentation, and, of course, by the standard (The C++11 Programming Language, §7.7.1).

This is a very simple fact that is really clear in the standard, but it is rarely used in code and referenced on the web.

The reason for that is that binding to a const ref instead of a const value – or even a plain value – seems useless.

But is it?

Binding a lambda to a const ref

Coming back to the initial context, the question was why it is legal to bind a lambda to a const ref and if it is useful.

As a reminder, here is the example I showed you earlier:

#include <vector>
#include <algorithm>

int count_even(const std::vector<int>& v)
{
    const auto& my_func = [] (int i)->bool
    {
        return i%2==0; 
    };

  	return std::count_if(std::cbegin(v), std::cend(v), my_func);
}

When we put it in C++ Insights (cppinsights.io), we obtain the following code:

#include <vector>
#include <algorithm>

int count_even(const std::vector<int, std::allocator<int> > & v)
{
    
  class __lambda_6_27
  {
    public: 
    inline /*constexpr */ bool operator()(int i) const
    {
      return (i % 2) == 0;
    }
    
    using retType_6_27 = auto (*)(int) -> bool;
    inline /*constexpr */ operator retType_6_27 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline bool __invoke(int i)
    {
      return (i % 2) == 0;
    }
    
    public: 
    // inline /*constexpr */ __lambda_6_27(const __lambda_6_27 &) noexcept = default;
    // inline /*constexpr */ __lambda_6_27(__lambda_6_27 &&) noexcept = default;
    // /*constexpr */ __lambda_6_27() = default;
    
  };
  
  const __lambda_6_27 & my_func = __lambda_6_27{};
  return static_cast<int>(std::count_if(std::cbegin(v), std::cend(v), __lambda_6_27(my_func)));
}

As you might have guessed, a lambda function is in fact a functor (here called __lambda_6_27). Thus, the assignation calls the constructor of that functor, which is a rvalue.

We just saw that we could bind a rvalue to a const ref, thus binding a lambda to a const ref is legal.

This is why we can bind a lambda to a const ref.

Performance and optimization

To answer the question that if we should bind a lambda to a const ref instead of a value, we have to evaluate if one method is faster that the other.

Execution time

I’ll use Quick C++ Benchmarks (quick-bench.com) to evaluate execution time.

Here are the snippets I’ll use:

std::vector<int> v = {0,1,2,3,4};

static void ConstRef(benchmark::State& state)
{
  const auto& l = [](int i)->bool{ return i%2 == 0;};
  for (auto _ : state)
  {
    std::count_if(cbegin(v), cend(v), l);
  }
}
BENCHMARK(ConstRef);

static void Plain(benchmark::State& state)
{
  auto l = [](int i)->bool{ return i%2 == 0;};
  for (auto _ : state)
  {
    std::count_if(cbegin(v), cend(v), l);
  }
}
BENCHMARK(Plain);

I ran benchmarks using Clang 11.0 and GCC 10.2, with all optimization options between -O0 and -O3.

And here are the results:

CompilerOptiomization
option
ET with
const ref
ET with
plain value
const ref / plain
value ratio
Clang 11.0-O032.30633.7320.958
Clang 11.0 -O1224.96204.921.097
Clang 11.0 -O23.9982e-64.0088e-60.997
Clang 11.0 -O33.7273e-64.1281e-60.903
GCC 10.2-O0 64.37965.0170.990
GCC 10.2 -O1 11.75411.8710.990
GCC 10.2 -O2 3.7470e-64.0196e-60.932
GCC 10.2 -O3 3.6523e-63.9021e-60.936

What you want to look at is the last column, which computes the ratio between const-ref and plain value (the units of the execution times have unlabeled units, it’s useless to try and compare them directly).

A value greater that 1 means the const-ref version is slower than the plain version, while a value lower that 1 means the const-ref is slower.

Here are the charts:

All in all we can see that while the const-ref version is still faster than the value version, the difference doesn’t go above 10%. The highest differences are at the maximum optimization level.

If the code in a bottleneck (in the 20% of Pareto’s law) then this 10% can make a little difference, but I wouldn’t expect more than one or two percent gain overall (including other parts of your code).

However, if this is not a bottleneck, then there’s no version to prefer over the other one.

Compilation time

Does the const ref binding can affect compilation time? To answer this, I used C++ Build Benchmarks (build-bench.com).

I compiled the following codes in the same circumstances, using Clang 11.0 and GCC 10.2, with all optimization options between -O0 and -O3:

#include <vector>

int main() 
{
    const auto& l = [](int i)->bool{ return i%2 == 0;};
    std::vector<int> v= {0,1,2,3,4};
    std::count_if(cbegin(v), cend(v), l);
}

Then, without using const ref:

#include <vector>

int main() 
{
    auto l = [](int i)->bool{ return i%2 == 0;};
    std::vector<int> v= {0,1,2,3,4};
    std::count_if(cbegin(v), cend(v), l);
}

And here are the results:

CompilerOptimization
option
BT with
const ref
BT with
plain value
const ref / plain
value ratio
Clang 11.0-O00.36290.35101.034
Clang 11.0 -O10.40100.40350.994
Clang 11.0 -O20.37550.37650.997
Clang 11.0 -O30.37450.37351.003
GCC 10.2 -O0 0.39150.39001.004
GCC 10.2 -O1 0.38300.38101.005
GCC 10.2 -O2 0.37650.37750.997
GCC 10.2 -O3 0.37650.37501.004

In any case, there is less than 4% difference between the two versions, and in most cases it’s event under 1%. We can say that there is no effective difference.

Conclusion

There is not real advantage in binding a rvalue to a const ref instead of a plain value. Most of the time, you’ll prefer to use a const value just for the sake of saving a symbol (but there is no huge difference).

In case you are in a bottleneck, you might consider using const refs instead of values, but I suggest you do your own benchmarks, tied to your specific context, in order to detemine which version is the better.

Thanks for reading, and see you next week!

Author: Chloé Lourseyre

Leave a Reply