C++11 "Auto" Semantics

C++11 auto semantics

The rule is simple : it is how you declare it.

int i = 5;
auto a1 = i; // value
auto & a2 = i; // reference

Next example proves it :

#include <typeinfo>
#include <iostream>

template< typename T >
struct A
{
static void foo(){ std::cout<< "value" << std::endl; }
};
template< typename T >
struct A< T&>
{
static void foo(){ std::cout<< "reference" << std::endl; }
};

float& bar()
{
static float t=5.5;
return t;
}

int main()
{
int i = 5;
int &r = i;

auto a1 = i;
auto a2 = r;
auto a3 = bar();

A<decltype(i)>::foo(); // value
A<decltype(r)>::foo(); // reference
A<decltype(a1)>::foo(); // value
A<decltype(a2)>::foo(); // value
A<decltype(bar())>::foo(); // reference
A<decltype(a3)>::foo(); // value
}

The output:

value
reference
value
value
reference
value

c++11 async continuations or attempt at .then() semantics

I find 3 problems with the above implemention:

  • It will only work if you pass std::shared_future as Fut.
  • The continuation might want a chance to handle exceptions.
  • It will not always behave as expected, since if you do not specify std::launch::async it might be deferred, thus the continuation is not invoked as one would expect.

I've tried to address these:

template<typename F, typename W, typename R>
struct helper
{
F f;
W w;

helper(F f, W w)
: f(std::move(f))
, w(std::move(w))
{
}

helper(const helper& other)
: f(other.f)
, w(other.w)
{
}

helper(helper&& other)
: f(std::move(other.f))
, w(std::move(other.w))
{
}

helper& operator=(helper other)
{
f = std::move(other.f);
w = std::move(other.w);
return *this;
}

R operator()()
{
f.wait();
return w(std::move(f));
}
};

}

template<typename F, typename W>
auto then(F f, W w) -> std::future<decltype(w(F))>
{
return std::async(std::launch::async, detail::helper<F, W, decltype(w(f))>(std::move(f), std::move(w)));
}

Used like this:

std::future<int> f = foo();

auto f2 = then(std::move(f), [](std::future<int> f)
{
return f.get() * 2;
});

C++11, move semantics of returning vectors functional style

I'm not sure why the memory address of the first element is the same after calling abs(), but there is a better way to write it. The code would be simpler if you use std::transform(), available in the algorithm header:

std::vector<float> abs(const std::vector<float> &v)
{
std::vector<float> abs_vec;

std::transform(v.begin(), v.end(), std::back_inserter(abs_vec),
[] (float val) {
return std::abs(val);
});

return abs_vec;
}

This will iterate over all of the elements in v, and for each one will call the lambda function. The return value of that function will be inserted onto the end of abs_vec.

The loop at the end of normalize() could also be rewritten to use std::transform(), as well. In this case, the norm_factor variable can be captured so it can be used within the lambda:

std::transform(v.begin(), v.end(), std::back_inserter(norm_vec),
[norm_factor] (float val) {
return val / norm_factor;
});

What is a lambda expression in C++11?

The problem

C++ includes useful generic functions like std::for_each and std::transform, which can be very handy. Unfortunately they can also be quite cumbersome to use, particularly if the functor you would like to apply is unique to the particular function.

#include <algorithm>
#include <vector>

namespace {
struct f {
void operator()(int) {
// do something
}
};
}

void func(std::vector<int>& v) {
f f;
std::for_each(v.begin(), v.end(), f);
}

If you only use f once and in that specific place it seems overkill to be writing a whole class just to do something trivial and one off.

In C++03 you might be tempted to write something like the following, to keep the functor local:

void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}

however this is not allowed, f cannot be passed to a template function in C++03.

The new solution

C++11 introduces lambdas allow you to write an inline, anonymous functor to replace the struct f. For small simple examples this can be cleaner to read (it keeps everything in one place) and potentially simpler to maintain, for example in the simplest form:

void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Lambda functions are just syntactic sugar for anonymous functors.

Return types

In simple cases the return type of the lambda is deduced for you, e.g.:

void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}

however when you start to write more complex lambdas you will quickly encounter cases where the return type cannot be deduced by the compiler, e.g.:

void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}

To resolve this you are allowed to explicitly specify a return type for a lambda function, using -> T:

void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) -> double {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}

"Capturing" variables

So far we've not used anything other than what was passed to the lambda within it, but we can also use other variables, within the lambda. If you want to access other variables you can use the capture clause (the [] of the expression), which has so far been unused in these examples, e.g.:

void func5(std::vector<double>& v, const double& epsilon) {
std::transform(v.begin(), v.end(), v.begin(),
[epsilon](double d) -> double {
if (d < epsilon) {
return 0;
} else {
return d;
}
});
}

You can capture by both reference and value, which you can specify using & and = respectively:

  • [&epsilon, zeta] captures epsilon by reference and zeta by value
  • [&] captures all variables used in the lambda by reference
  • [=] captures all variables used in the lambda by value
  • [&, epsilon] captures all variables used in the lambda by reference but captures epsilon by value
  • [=, &epsilon] captures all variables used in the lambda by value but captures epsilon by reference

The generated operator() is const by default, with the implication that captures will be const when you access them by default. This has the effect that each call with the same input would produce the same result, however you can mark the lambda as mutable to request that the operator() that is produced is not const.

Why does auto return type lose move semantics in this example?

I think Nicolai could have just phrased it a bit better.

When you return by auto, your function returns a value (the type of which will be deduced). If std::invoke returns a pure rvalue or an xvalue, then of course the result of call will be constructed accordingly (by a move, if possible). We don't "lose move semantics" in that sense.

But when we return by value, that value object needs to be created. It can be created by a move, and under certain circumstances (which aren't present here) it can be elided, but it must be created. Guaranteed copy elision doesn't allow us to cop out of creating this object.

That can be quite wasteful if std::invoke gives us back an xvalue (an rvalue reference to something). Why should we construct an object to return it?

That's why a slide or two later he says we should return by decltype(auto). The deduction rules will then preserve the value cateogry of the the call to std::invoke:

  1. If it returns a value, we are no worse off. Our own call will return a value (which it may create by moving the return value of std::invoke).

  2. If std::invoke returns an lvalue(X&) or an xvalue(X&&), that will be returned as is, without creating another object by a copy or move.

What does `auto &&i = foo();` mean

auto&& (just like T&& in parameter of a function template where T is a template parameter of that function template) follows slightly different rules than other deductions - it's unofficially called a "universal reference."

The idea is that if the initialiser is an lvalue of type X, the auto is deduced to X&. If it's an rvalue of type X, the auto is deduced to X. In both cases, && is then applied normally. From reference collapsing rules, X& && becomes X&, while X && remains X&&.

This means that in your a1 case, auto is indeed deduced to int, but a1 is then naturally declared with type int&&, and that's what decltype(a1) gives you.

At the same time, the auto in a2 is float&, and so is the type of a2, which the decltype(a2) again confirms.

In other words, your expectation that auto -> int in the first case is correct, but the type of a1 is auto &&a1, not just auto a1.

c++11 async continuations or attempt at .then() semantics

I find 3 problems with the above implemention:

  • It will only work if you pass std::shared_future as Fut.
  • The continuation might want a chance to handle exceptions.
  • It will not always behave as expected, since if you do not specify std::launch::async it might be deferred, thus the continuation is not invoked as one would expect.

I've tried to address these:

template<typename F, typename W, typename R>
struct helper
{
F f;
W w;

helper(F f, W w)
: f(std::move(f))
, w(std::move(w))
{
}

helper(const helper& other)
: f(other.f)
, w(other.w)
{
}

helper(helper&& other)
: f(std::move(other.f))
, w(std::move(other.w))
{
}

helper& operator=(helper other)
{
f = std::move(other.f);
w = std::move(other.w);
return *this;
}

R operator()()
{
f.wait();
return w(std::move(f));
}
};

}

template<typename F, typename W>
auto then(F f, W w) -> std::future<decltype(w(F))>
{
return std::async(std::launch::async, detail::helper<F, W, decltype(w(f))>(std::move(f), std::move(w)));
}

Used like this:

std::future<int> f = foo();

auto f2 = then(std::move(f), [](std::future<int> f)
{
return f.get() * 2;
});

Why do I need to explicitly write the 'auto' keyword?

Dropping the explicit auto would break the language:

e.g.

int main()
{
int n;
{
auto n = 0; // this shadows the outer n.
}
}

where you can see that dropping the auto would not shadow the outer n.



Related Topics



Leave a reply



Submit