Домівка > Перекладені > Лямбда-вирази, auto та static_assert: C++0x можливості в VC10

Лямбда-вирази, auto та static_assert: C++0x можливості в VC10

Лямбда-вирази

В C++0x, “лямбда-вирази” непрямим чином створюють функцональні об’єкти, які потім поводяться як функціональні об’єкти написані вручну. Наведемо “Hello, World” лямбда-вираз:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

0 1 2 3 4 5 6 7 8 9

[] це вступні символи, що кажуть компілятору про те, що починається визначення лямбда-виразу. (int n) це оголошення лямбда-параметра. Зрештою, { cout << n << " "; } це складений вислів, який править за тіло безіменного функціонального об’єкта. За не вказанням іншого, оператор виклику функціонального об’єкта повертає void.

Тож, C++0x подумки перекладає це в код, який ви мали б написати в C++98:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

struct LambdaFunctor {
    void operator()(int n) const {
        cout << n << " ";
    }
};

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    for_each(v.begin(), v.end(), LambdaFunctor());
    cout << endl;
}

0 1 2 3 4 5 6 7 8 9

Відтепер замість казати “безіменний функціональний об’єкт” будемо казати “лямбда”, але важливо пам’ятати, що лямбда-вирази роблять: визначають класи і створюють об’єкти.

Звісно, складений вислів лямбди може містити декілька тверджень:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    for_each(v.begin(), v.end(), [](int n) {
        cout << n;

        if (n % 2 == 0)
            cout << " even ";
        else
            cout << " odd ";
    });
    cout << endl;
}

0 even 1 odd 2 even 3 odd 4 even 5 odd 6 even 7 odd 8 even 9 odd

Лямбда може повертати не тільки void. Якщо тіло лямбди містить return expression, тоді тип, що повертається буде автоматично виведений з типу expression:

#include <algorithm>
#include <deque>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    deque<int> d;

    transform(v.begin(), v.end(), front_inserter(d), [](int n) { return n * n * n; });

    for_each(d.begin(), d.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

729 512 343 216 125 64 27 8 1 0

Тут, типом n * n * n є int,отже виклик цього лямбда-виразу повертає int.

Для лямбд з більш складними складеними висловами не виходить автоматично вивести тип результату. Ви маєте вказати його явно:

#include <algorithm>
#include <deque>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v; 

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    deque<double> d; 

    transform(v.begin(), v.end(), front_inserter(d), [](int n) -> double {
        if (n % 2 == 0)
            return n * n * n;
        else
            return n / 2.0;
    });

    for_each(d.begin(), d.end(), [](double x) { cout << x << " "; });
    cout << endl;
}

4.5 512 3.5 216 2.5 64 1.5 8 0.5 0

-> double це необов’язковий тип результату в лямбда-виразі. Чому він не пишеться з лівого боку, як роблять програмісти на С/С++ ? Бо тоді вступні символи не будуть першими, і компілятор не буде знати, що лямбда-вираз почався.

Якщо ви забули вказити тип результату, компілятор поскаржиться на кожний return:
lambda.cpp(20) : error C3499: a lambda that has been specified to have a void return type cannot return a value
lambda.cpp(22) : error C3499: a lambda that has been specified to have a void return type cannot return a value

Усі лямбди подані вище не зберігали стан: вони не містили членів з даними. Ви можете використовувати лямбди, що зберігають стан, це робиться через “захоплення” локальних змінних. Порожні вступні дужки кажуть “У мене не має станів”. Але в цих дужках ви можете вказати список-захвату:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 0;
    int y = 0;

    // op>>() залишає нові лінії в вхідному потоці,
    // що може вносити плутанину. Я рекомендую
    // уникати цього, натомість використовувати не член
    // getline(cin, str) для прочитання цілих ліній
    // і тоді розбирати їх. Але задля стислості коду,
    // я використаю op>>():

    cout << "Input: ";
    cin >> x >> y;
    v.erase(remove_if(v.begin(), v.end(), [x, y](int n) { return x < n && n < y; }), v.end());

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9

Якщо ви забули про список-захвату, компілятор поскаржиться:
lambda.cpp(27) : error C3493: 'x' cannot be implicitly captured as no default capture mode has been specified
lambda.cpp(27) : error C3493: 'y' cannot be implicitly captured as no default capture mode has been specified

(Незабаром я розкажу про типовий захват)

Пам’ятайте, лямбда-вираз безіменний клас функціонального об’єкта. Складений вислів { return x < n && n < y; } слугує як тіло функції, яку викликають в цьому класі. Хоча складений вислів лексично знаходиться в самій функції main(), концептуально він знаходиться поза нею, отже ви не можете використовувати локальні змінні функції main() без захвату.

Наведомо у що подумки перетворюється попередній код:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>

using namespace std;

class LambdaFunctor {
public:
    LambdaFunctor(int a, int b) : m_a(a), m_b(b) { }
    bool operator()(int n) const { return m_a < n && n < m_b; }

private:
    int m_a;
    int m_b;
};

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 0;
    int y = 0;

    cout << "Input: ";
    cin >> x >> y; // EVIL!

    v.erase(remove_if(v.begin(), v.end(), LambdaFunctor(x, y)), v.end());

    copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9

Тут добре видно, що захоплення відбувається за значенням. Копії локальниз змінних зберігаються в самому функціональному об’єкті. Це дозволяє функціональному об’єкту пережити локальні змінні, які були захоплені для його створення. Однак, наголосимо, що (а) захоплені об’єкти не можуть бути змінені в лямбді, бо за замовчанням оператор виклику константний, (б) вартість копіювання деяких об’єктів дуже висока та (в) зміна локальних змінних не відіб’ється на захоплених копіях (це звичайно для передачі за значенням). Скоро я покажу як чинити з усім цим якщо виникне потреба в відмінній поведінці.

Але спочатку, замість визначення, які саме змінні захоплювати ви можете просто дати вказівку на захоплення всього за значенням. Для цього використовується такий лямбда-вступник [=] (типовий захват = призначений, щоб ви думали про надавання значення або ініціалізацію через копіювання Foo foo = bar;, хочу копії утворюються через пряму ініціалізацію, як m_a(a) у прикладі вище):

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 0;
    int y = 0;

    cout << "Input: ";
    cin >> x >> y; // Зло!

    v.erase(remove_if(v.begin(), v.end(), [=](int n) { return x < n && n < y; }), v.end());

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9

Коли компілятор бачить x і y згадані в лямбді, він захоплює їх з main() за значенням.

А як щодо (a), зміни захоплених копій? За не вказанням іншого, виклик константний, але ви можете зробити його не константним застосувавши mutable:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;
 
    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 1;
    int y = 1;

    for_each(v.begin(), v.end(), [=](int& r) mutable {
        const int old = r;
        r *= x * y;
        x = y;
        y = old;
    });

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;

    cout << x << ", " << y << endl;
}

0 0 0 6 24 60 120 210 336 504
1, 1

Цей код множить кожний елемент v на попередні два елементи. (Я думав, що буде важко віднайти приклад, який не може бути здійснений через partial_sum(), що може помножити на всі попередні елементи, або adjacent_difference(), що може помножити на попередній елемент.) Зауважте, що (г) зміна захопленої копії не відбивається на локальній змінній (знов через передачу за значенням).

Що станеться, якщо ви захочете зміни пункти (б), (в) і (г): уникнути копіювання, спостерігати за змінами локальних змінних з середини лямбди і змінювати локальні змінні з середини лямбди? В такому разі, ми маємо захоплювати за посиланням (by reference). Таке захоплення забезпечує лямбда-вступник [&x, &y] (ви маєте думати про це як про X& x, Y& y ; що є, “посиланням”, але не “адресою”):

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 1;
    int y = 1;

    for_each(v.begin(), v.end(), [&x, &y](int& r) {
        const int old = r;
        r *= x * y;
        x = y;
        y = old;
    });

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;

    cout << x << ", " << y << endl;
}

0 0 0 6 24 60 120 210 336 504
8, 9

Зауважте,що відмінність від попереднього прикладу (1) лямбда-вступник [&x, &y] , (2) відсутність mutable і (3) локальні змінні x і y наприкінці мають значення 8 і 9, що відображає їхні зміни в лямбді.

Подумки це перетворюється на:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>

using namespace std;

#pragma warning(push)
#pragma warning(disable: 4512) // не зміг згенерувати оператор надавання значення

class LambdaFunctor {
public:
    LambdaFunctor(int& a, int& b) : m_a(a), m_b(b) { }

    void operator()(int& r) const {
        const int old = r;
        r *= m_a * m_b;
        m_a = m_b;
        m_b = old;
    }

private:
    int& m_a;
    int& m_b;
};

#pragma warning(pop)

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int x = 1;
    int y = 1;

    for_each(v.begin(), v.end(), LambdaFunctor(x, y));

    copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
    cout << endl;

    cout << x << ", " << y << endl;
}

0 0 0 6 24 60 120 210 336 504
8, 9

(Коли ви використовуєте лямбди, компілятор автоматично відключає попередження C4512 для визначень лямбд.)

Коли ви захоплюєте локальні змінні за посиланнями, ви уникаєте копіювання, що дозволяє функціональному об’єктові реагувати на зміни локальних змінних, а також самому змінювати їх. (Зверніть увагу, що оператор виклику константний, бо ми не сказали mutable, але це унеможливлює зміни його членів даних. Тут таким членом є посилання, і воно не може бути зміненим у будь-якому випадку, але його можно використати для зміни того, на що воно посилається. Константність оператора виклику неглибока, як завжди.)

Звісно, якщо лямбда функціональний об’єкт переживає локальні змінні, які він захопив за посиланням, ви отримаєте падіння.

І знов, ви можете використовувати захвати за невказанням іншого; лямбда-вступник [&] каже “захопити все за посиланнями”.

А як ви хочемо поєднати захоплювання за значенням і за посиланням? Ви можете сказати [a, b, c, &d, e, &f, g]. Але ви також можете замовити захват за невказанням іншого, а тоді перевизначити його для окремих локальних змінних. Ось приклад:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    int sum = 0;
    int product = 1;
    int x = 1;
    int y = 1;

    for_each(v.begin(), v.end(), [=, &sum, &product](int& r) mutable {
        sum += r;
        if (r != 0)
            product *= r;
        const int old = r;
        r *= x * y;
        x = y;
        y = old;
    });

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;

    cout << "sum: " << sum << ", product: " << product << endl;
    cout << "x: " << x << ", y: " << y << endl;
}

0 0 0 6 24 60 120 210 336 504
sum: 45, product: 362880
x: 1, y: 1

Тут ми хочемо захопити x і y за значенням (бо ми хочемо змінювати їх в лямбді, але не зовні), а sum і product за посиланнями (бо ми хочемо змінювати їх і зовні). by reference (because we do want to modify them outside). Протилежний лямбда-вступник [&, x, y] видасть той самий результат (захопить все за посиланнями, тільки x і y за значеннями).

Тепер, що якщо ви захочете зробити це?

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

class Kitty {
public:
    explicit Kitty(int toys) : m_toys(toys) { }

    void meow(const vector<int>& v) const {
        for_each(v.begin(), v.end(), [m_toys](int n) {
            cout << "Якщо ви дасте мені " << n << " іграшок, я матиму " << n + m_toys << " іграшок загалом." << endl;
        });
    }

private:
    int m_toys;
};

int main() {
    vector<int> v;

    for (int i = 0; i < 3; ++i)
        v.push_back(i);

    Kitty k(5);
    k.meow(v);
}

lambda.cpp(12) : error C3480: 'Kitty::m_toys': a lambda capture variable must be from an enclosing function scope

Правопис лямбда-виразів дозволяє захоплювати локальні змінні, але члени-дані не є локальними змінними. За допомогою особливого дозволу ви можете отримати їх:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

class Kitty {
public:
    explicit Kitty(int toys) : m_toys(toys) { }

    void meow(const vector<int>& v) const {
        for_each(v.begin(), v.end(), [this](int n) {
            cout << "Якщо ви дасте мені " << n << " іграшок, я матиму " << n + m_toys << " іграшок загалом." << endl});
    }

private:
    int m_toys;
};

int main() {
    vector<int> v;

    for (int i = 0; i < 3; ++i)
        v.push_back(i);

    Kitty k(5);
    k.meow(v);
}

Якщо ви дасте мені 0 іграшок, я матиму 5 іграшок загалом.
Якщо ви дасте мені 1 іграшок, я матиму 6 іграшок загалом.
Якщо ви дасте мені 2 іграшок, я матиму 7 іграшок загалом.

Коли ви захопили this, m_toys як завжди рівнозначно this->m_toys. Ви також можете явно вказати this->m_toys. (В самій лямбді, ви можете використати this тільки, якщо ви захопили його; ви ні в який спосіб не можете отримати вказівник на сам лямбда-об’єкт.)

Ви також можете здійснити захоплення неявно:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

class Kitty {
public:
    explicit Kitty(int toys) : m_toys(toys) { }

    void meow(const vector<int>& v) const {
        for_each(v.begin(), v.end(), [=](int n) {
            cout << "Якщо ви дасте мені " << n << " іграшок, я матиму " << n + m_toys << " іграшок загалом." << endl});
        });
    }

private:
    int m_toys;
};

int main() {
    vector<int> v;

    for (int i = 0; i < 3; ++i)
        v.push_back(i);

    Kitty k(5);
    k.meow(v);
}

Якщо ви дасте мені 0 іграшок, я матиму 5 іграшок загалом.
Якщо ви дасте мені 1 іграшок, я матиму 6 іграшок загалом.
Якщо ви дасте мені 2 іграшок, я матиму 7 іграшок загалом.

Також ви можете написати [&], але це не вплине на те як відбудеться захоплення (завжди за значенням). Ви не можете написати [&this].

Якщо вам потрібне оголошення лямбди без параметрів, ви можете зовсім вилучити оголошення лямбда параметрів:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>

using namespace std;

int main() {
    vector<int> v;

    int i = 0;

    generate_n(back_inserter(v), 10, [&] { return i++; });

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;

    cout << "i: " << i << endl;
}

0 1 2 3 4 5 6 7 8 9
i: 10

Це на два символи коротше від [&]() { return i++; }. Вам вирішувати коли відсутність оголошення параметрів буде добрим стилем.

Для сміху, можна написати наступні вірні C++0x рядки:

int main() {
    [](){}();
    []{}();
}

Цей код створює дві лямбди без дій (одну з оголошенням параметрів, другу без) і одразу ж викликає їх (за допомогою останніх круглих дужок).

Зауважте, що необов’язкове оголошення лямбда-параметрів має такий правопис:

(lambda-parameter-declaration-listopt)mutableopt exception-specificationopt lambda-return-type-clauseopt

Отже, якщо ви хочете використати mutable або -> ReturnType, вам потрібні порожні дужки між ними і лямбда-вступником.

Насамкінець, через те, що лямбди утворюють звичайні функціональні об’єкти, ви можете зберігати їх у tr1::function:

#include <algorithm>
#include <functional>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;
using namespace std::tr1;

 

void meow(const vector<int>& v, const function<void (int)>& f) {
    for_each(v.begin(), v.end(), f);
    cout << endl;
}

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i)
        v.push_back(i);

    meow(v, [](int n) { cout << n << " "; });
    meow(v, [](int n) { cout << n * n << " "; });

    function<void (int)> g = [](int n) { cout << n * n * n << " "; };

    meow(v, g);
}

0 1 2 3 4 5 6 7 8 9
0 1 4 9 16 25 36 49 64 81
0 1 8 27 64 125 216 343 512 729

auto

Ключове слово auto відоме ще з C++98, де воно нічогісінько не робило, отримало нове призначення в C++0x в автоматичному виведенні типів. Коли воно використовується в оголошеннях, воно каже наступне “зробити тип таким самим як типи ініціалізатора”. Ось:

#include <iostream>
#include <map>
#include <ostream>
#include <regex>
#include <string>

using namespace std;
using namespace std::tr1;

int main() {
    map<string, string> m;

    const regex r("(\\w+) (\\w+)");

    for (string s; getline(cin, s); ) {
        smatch results;
        if (regex_match(s, results, r))
            m[results[1]] = results[2];
    }

    for (auto i = m.begin(); i != m.end(); ++i)
        cout << i->second << " are " << i->first << endl;
}

cute kittens
ugly puppies
evil goblins
^Z
kittens are cute
goblins are evil
puppies are ugly

map::iterator, настав кінець твому десятилітньому терору!

(Зверніть увагу, що m.begin() повертає ітератор, а не константний ітератор, бо map не константна. cbegin() у C++0x дозволяє вам отримати константний ітератор з неконстантного контейнера.)

Лямбди та auto

Раніше, я згадував про зберігання лямбд у in tr1::functions. Але не варто робити так, хіба це дуже необхідно, бо tr1::function потребує додаткових витрат. Якщо ви хочете повторно використати лямбду або просто хочете надати їй ім’я, ви можете використати auto. Ось приклад, який додатково робить дещо цікаве:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

template <typename T, typename Predicate> void keep_if(vector<T>& v, Predicate pred) {
    auto notpred = [&](const T& t) {
        return !pred(t);
    };

    v.erase(remove_if(v.begin(), v.end(), notpred), v.end());
}

template <typename Container> void print(const Container& c) {
    for_each(c.begin(), c.end(), [](const typename Container::value_type& e) { cout << e << " "; });
    cout << endl;
}

int main() {
    vector<int> a;

    for (int i = 0; i < 100; ++i)
        a.push_back(i);

    vector<int> b;

    for (int i = 100; i < 200; ++i)
        b.push_back(i);

    auto prime = [](const int n) -> bool {
        if (n < 2)
            return false;

        for (int i = 2; i <= n / i; ++i)
            if (n % i == 0)
                return false;
        return true;
    };

    keep_if(a, prime);
    keep_if(b, prime);

    print(a);
    print(b);
}

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199

notpred це інвертор лямбди! Зауважте, що ми не можемо використати C++98 not1(), бо це вимагає від предиката бути спадкоємцем unary_function, а лямбда таким не є.

static_assert

static_assert дозволяє вам створювати помилки компіляції з вашими повідомленнями про помилку:

template <int N> struct Kitten {
    static_assert(N < 2, "Kitten<N> requires N < 2.");
};

int main() {
    Kitten<1> peppermint;
    Kitten<3> jazz;
}

lambda.cpp(2) : error C2338: Kitten requires N < 2.
  lambda(8) : see reference to class template instantiation 'Kitten' being compiled
  with
  [
    N=3
  ]

Вихідна стаття

Категорії:Перекладені Позначки:,
  1. Коментарів ще немає.
  1. No trackbacks yet.

Залишити коментар