Details About Some of the New C++ Language Features

Yuriy Solodkyy

When you read our release notes, you’ll notice that we’ve added a large number of C++ language features. The details of what these features are can be found all over the web, but what purpose they serve might still be nebulous. This article will attempt to explain the origins of and how to use a few of these new language mechanisms for best results.

Terse Range-Based For Loops

[Note 11/12/2014 1:00pm PST: This proposed feature was approved by the Evolution and Core Working Groups in June 2014, and it was implemented in VS 2015 Preview and Clang 3.5, but the full Standardization Committee rejected this feature on Nov. 7, so it will be removed from VS 2015 RTM. New syntax will be proposed at the next Committee meeting in May 2015.]

This proposed C++17 feature allows you to omit the type of the element in range-based for loops:

    std::vector<int> v = { 1, 2, 3 };
    for (i : v)
    {
        printf(“%d “, i);
    }

You should keep in mind that this is more than a mere syntactic convenience.

Many developers will tend to write auto as the element type, and in most cases this would work correctly, except that auto by itself causes the elements to be copied, which is usually not desirable. Furthermore, mutations to the element will not be reflected in the container.

Some developers might try to get around these problems by writing auto & as the element type instead. This will indeed work in a greater number of cases, but in some corner cases it also breaks:

    std::vector<bool> v = { true, false };
    for (auto &i : v) //C4239
    {
        printf(“%d “, (bool)i);
    }

The std::vector<bool> type is specialized to be a bit vector, so a proxy type (a.k.a. “simulated reference”) is needed when iterating. The compiler will emit:

warning C4239: nonstandard extension used: ‘initializing’: conversion from ‘std::_Vb_reference<std::_Wrap_alloc<std::allocator<_Other>>>’ to ‘std::_Vb_reference<std::_Wrap_alloc<std::allocator<_Other>>> &’
    with
    [
        _Other=std::_Vbase
    ]

If that extension were disabled, this code would result in a compilation error.

For these reasons, a terse range-based for loop behaves the same as if you had written auto && as the type of the element. This ensures that modifying the elements of the container will work right in cases where such modifications make sense, and that proxy types are handled correctly too.

Generalized Lambda Capture

To understand the reason for introducing this feature, also known as “init-capture”, imagine that you wish to generate some heap-allocated data to be operated on later with a lambda function:

auto f(int i)
{
    std::unique_ptr<int> p = std::make_unique<int>(i);
    return [=](int j){ return *p + j; }; //C2280
}
 
int main()
{
    auto g = f(42);
    printf(“%dn”, g(2));
    printf(“%dn”, g(12));
    return 0;
}

Unfortunately, this will not work. The compiler will emit:

error C2280: ‘std::unique_ptr<int,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,std::default_delete<_Ty>> &)’: attempting to reference a deleted function

Because unique_ptr is a move-only type, capturing it from the enclosing scope by value is disallowed because it would require a copy.

You could try to get around this via capturing by reference instead, replacing the return statement of f with:

    return [&](int j){ return *p + j; };

But p goes out of scope once f exits, so when the program is run, the output is gibberish, reflecting whatever garbage was on the stack at the time:

1160179346
681009

To truly solve this problem, you need a way to capture an instance of a move-only type by value. To do so, you can use generalized lambda capture, replacing the return statement of f with:

    return [q = std::move(p)](int j){ return *q + j; };

The italicized code moves the value of p into q, which is stored directly in the lambda function itself and thus will not expire until the lambda itself goes out of scope (in this case, at the end of main). The program now runs as expected and prints:

44
54

Dining Philosophers

The “Dining Philosophers Problem” is a common scenario used to illustrate deadlock in concurrent resource allocation. For more details, read this Wikipedia article or one of the many other descriptions found on the internet or in a textbook.

Initial implementation

This uses the resource hierarchy solution to avoid deadlock.

#include <cstdio>
#include <thread>
#include <mutex>
#include <vector>
 
class Fork
{
private:
    const int position;
    std::mutex m;
 
public:
    Fork(int position) : position(position) {}
 
    bool pickup(int position) {
        if (m.try_lock()) {
            printf(“Philosopher %d picked up fork %d.n”, position, this->position);
            return true;
        }
        return false;
    }
 
    void drop(int position) {
        m.unlock();
        printf(“Philosopher %d dropped fork %d.n”, position, this->position);
    }
};
 
class Philosopher
{
private:
    const int position;
    const std::vector<Fork *> *forks;
 
public:
    Philosopher(int position, std::vector<Fork *> *forks) : position(position), forks(forks) {}
 
    void think() { printf(“Philosopher %d is thinking.n”, position); _sleep(10); }
 
    void eat() { printf(“Philosopher %d is eating.n”, position); _sleep(10); }
 
    void start() {
        // assume that the number of forks matches the number of philosophers
        int left = position;
        int right = (position + 1) % forks->size();
 
&

0 comments

Discussion is closed.

Feedback usabilla icon