lamda, auto, decltype–”синтаксический сахар“ который сделает вашу жизнь проще

Наверно только ленивый еще не писал об этих нововведениях в C++11. Что ж, я не буду исключением и тоже напишу о них. Т.к. на мой взгляд эти вещи настолько изменяют стиль написания C++ программ, что они должны занимать одну из первых строк в рейтинге нововведений нового стандарта. С помощью этих средств использование STL становится более удобным, в то же время убирая необходимость в некоторой части STL. Я объединил эти три понятия в одной статье, т.к. считаю, что lambda и auto идут рука об руку; ну а decltype я добавил за компанию т.к. они несколько похожи с auto.

auto

В прошлых редакциях стандарта это ключевое слово означало, что объект должен быть создан в стековой памяти. Это древнее понятие, доставшееся в наследство от C, которое совершенно не нужно в современном C++, где все локальные переменные функций кладутся в стековую память по умолчанию.

void foo()
{
    int i;
    auto int j;
}

т.е объявление переменных i и j было эквивалентно в компиляторах, не обремененных знанием C++11. Сейчас же все изменилось и ключевое слово auto из бесполезного архаизма превратилось в глашатая нового подхода к разработке на C++. C пришествием C++11, - auto стало означать буквально следующее: ключевое слово auto говорит компилятору самостоятельно определить тип конечного выражения и подставить его вместо себя.

К примеру,

std::vector<int> Vec;
//Следующее выражения является rvalue типа std::vector<int>::iterator
Vec.begin(); 

Итак, компилятор знает, что за тип получается в результате Vec.begin() и следовательно может подставить его без нашей помощи:

std::vector<int>::iterator It = Vec.begin();//Так мы писали раньше...
auto It = Vec.begin();//...а так мы можем писать сейчас!

Две вышеприведенные строки семантически абсолютно идентичны и для них будет сформирован одинаковый код. Да, auto является синтаксическим сахаром, в данном случае(об еще одном случае поговорим позже). Но этот синтаксический сахар может сделать наш код куда чище, чем он был с нагромождением типов, под-типов и namespace’ов. Если вышеприведенный случай еще не так страшен, то можно привести более “тяжелый” случай:

boost::asio::ip::tcp::resolver::query query("google.com", "80");
//It имеет тип boost::asio::ip::tcp::resolver::iterator
auto It = resolver.resolve(query);

Тут уже лучше видно преимущество использования auto. А если учесть, что это еще можно использовать в цикле for, что значительно сократит длину его записи и станет проще для восприятия.

Честно говоря, использование for в современном C++ не лучшая идея. Но об этом мы поговорим позже.

Важно понимать, что auto это конструкция уровня компилятора и не имеет никакого отношения к RTTI. Т.е. тип определяется на этапе компиляции, а значит никакого выхода за пределы статической типизации тут нет. Однажды  присвоенное значение переменной определяет его тип на все время жизни и нельзя в эту переменную поместить данные, которые не совместимы с её типом.

std::vector<int> Vec1;
std::vector<double> Vec2;
auto It = Vec1.begin();
It = Vec2.end();//Ошибка, It имеет тип std::vector<int>::iterator

Теперь перейдем от бочки меда к ложке дегтя. Как и много другое в C++ auto имеет свое место и лепить его везде является плохим тоном, и вы рискуете быть проклятым людьми, которым придется работать с кодом перенасыщенным auto. Необходимо соблюдать баланс и выработать некую внутреннюю дисциплину по использованию auto. К примеру, использовать это ключевое слово только для сложных типов, когда это целесообразно.

На этом закончим с первой частью применения auto, как “синтаксического сахара” и перейдем к

decltype

Суть этого оператора проста: decltype определяет тип выражения, которые передается ему в качестве аргумента. Результатом выражения decltype является полученный тип. Довольно специфичный оператор, который, безусловно, найдет свих пользователей, но, в повседневной работе программиста-прикладника вряд-ли необходим. Самым банальным примером его применения мне представляется следующий код:

#include <iostream>
#include <string>
#include <sstream>

std::string operator+(const std::string& Lhs, int Rhs)
{
    std::ostringstream Stream(Lhs, std::ios_base::out | std::ios_base::ate);
    Stream << Rhs;
    return Stream.str();
}

template <class T, class U>
auto Mix(T Lhs, U Rhs) -> decltype(Lhs + Rhs)
{
    return Lhs + Rhs;
}

int main()
{
    std::string str("20");
    std::cout << Mix(str, 12) << " and the type is " <<
         typeid(decltype(Mix(str, 12))).name();
    return 0;
}

Да, этот пример надуман и нереалистичен, но он всецело показывает возможности decltype. Область применения ему найдется, если у вас будет подходящая задача.

lambda

Наконец мы подошли к основной части статьи. В С++ lambda функция является анонимной функцией способной хранить состояние. Также ей присущи свойства обычных функций. lambda функцию можно считать функтором без имени(тем более, что внутренняя реализации как раз им и является). Т.к. функция анонимна мы не можем декларировать её в одном месте и использовать в другом. Мы не можем определить её тип, т.к. тип lambda функции известен только компилятору. Вот тут нам и пригождается ключевое слово auto, без которого невозможно дать имя нашей функции.

Напомню, что функтором в языке C++ называется класс переопределяющий operator()

Пример.

int main()
{
    int First = 10;
    auto Printer = [First](int Second) -> int
    {
        std::cout << First + Second;
        return First + Second;
    };
    Printer(5);
    return 0;
}

Выделим lambda функцию отдельно:

auto Printer = [First](int Second) mutable -> int
{
    std::cout << First + Second;
    First++;
    return First + Second;
};

 

Синтаксис

Запишем синтаксис lambda в форме расширенной БНФ грамматики:

lambda ::= “[“ список захвата ”]” , [ “(“ аргументы ”)” ], [ “mutable” ], [ “nothrow” ], [ тип возвращаемого значения ] , “{” тело функции “}”

Список захвата

Содержит имена переменных, которые должны быть видимы внутри lambda функции. Переменные должны быть видимы в точке объявления lambda функции. Переменные перечисленные с модификатором ‘&’(амперсанд), не копируются в lambda функцию, а передаются лишь ссылки на эти переменные. Исходя из этого, время жизни переменной должно превышать время жизни lambda функции для гарантии предсказуемого поведения. Переменные перечисленные без модификаторов копируются в lambda функцию. Есть три специальных литерала, которые могут быть использованы в списке захвата:

  • this – Используется для возможности использования методов класса в lambda функции без явного указания this при вызове метода(например foo(), а не this->foo()). Может быть использован только в декларациях lambda функций внутри функций-членов.
  • = – Копирует все переменные из области видимости внутрь lambda функции. Не может быть использован совместно с литералом “&”. Если присутствует литерал “=”, то если внутри списка захвата присутствуют переменные они должны быть с модификатором “&”
  • & – Создает ссылки на все переменные из области видимости внутри lambda функции. Не может быть использован совместно с литералом “=”. Если присутствует литерал “&”, то если внутри списка захвата присутствуют переменные они должны быть без модификатора “&”

Важно знать, что при применении литералов “=” или “&” не происходит захвата всех локальных переменных, которые находятся в области видимости lambda функции. Захватываются лишь те переменные, которые затем используются непосредственно в этой lambda функции или же во вложенной в неё.

Примеры, псевдокод:

int x, y, z;
[x, y, &z] – скопировать x и y. Создать ссылку на z
[=, &z] – скопировать x и y. Создать ссылку на z
[&, y] – скопировать y. Создать ссылки на z и x
[&, &y] – Ошибка. y должен быть без модификатора
[&, =] – Ошибка.
[=, x] – Ошибка. x должен быть с модификатором "&"

Пример с this:

class A
{
    int i;
public:
    void foo()
    {
        auto Func = [this]
        {
            i = 3;
            bar();
        };
        Func();
    }
    void bar()
    {
        std::cout << i;
    }
};

 

аргументы

Тоже самое, что и в любой другой функции. Никаких отличий. Может быть опущен вместе с обрамляющими круглыми скобками; этот случай эквивалентен отсутствию аргументов, т.е. пустым “()”. Может быть опущен, только если отсутствуют все последующие части, вплоть до тела функции.

mutable

Перед тем как объяснить назначение этого ключевого слова в данном контексте необходимо пояснить одно свойство lambda функций. По умолчанию, lambda функция является константной по отношению к переменным, которые были захвачены без модификатора “&”, следовательно, нельзя изменять переменные, которые были переданы в lambda функцию с помощью литерала “=” или имени переменной. Если же, программист хочет изменять копии переменных в lambda функции он применяет ключевое слово mutable.

Примеры:

int main()
{
    int i = 0;
    auto First = [=]
    {
        i = 3;//Ошибка
    };
    auto Second = [=]() mutable
    {
        i = 3;
    };
    return 0;
}

тип возвращаемого значения

Запись вида –> type

Может быть опущен в двух случаях:

  • return отсутствует в теле функции. В этом случае это равносильно –> void
  • тело функции состоит из одной строки исполнения и это строка return(например return x + y;). Если принять во внимание пример, то подобная запись будет равносильна –> decltype(x + y)

В остальных случаях наличие типа возвращаемого значения обязательно.

Примеры:

int main()
{
    int i = 5;
    auto Sum = [=](int j)// -> decltype(i + j)
    {
         return i + j;
    };
    auto Print = [=]//() -> void
    {
        std::cout << Sum(6);
    };
    auto ComplexSum = [&](int j) -> int
    {
        i += 3;
        return i + j;
    };
    return 0;
}

тело функции

Обычный блок исполнения, ничего необычного.

Еще одной особенностью lambda функции есть возможность конвертации оной в указатель на функцию. Правда, здесь есть одно ограничение: в указатель на функцию может быть сконвертирована только lambda список захвата которой пуст. Т.е. lambda функция без состояния.

Пример:

int main()
{
    int i = 3;
    auto Stateful = [i](int j) 
    {
        std::cout << i - j;
    };
    auto Stateless = [](int j) 
    {
        int i = 4;
        std::cout << i - j;
    };
    void (*Ptr)(int j) = Stateless;
    Ptr  = Stateful;//Ошибка
    return 0;
}

 

Примеры применения lambda функций

В качестве первого примера, реализуем Active Object паттерн. Этот паттерн реализует идею отделения потока исполнения от основного потока. В данном примере показывается использование lambda  в союзе с новыми возможностями по работе с потоками.

ActiveObject.h

#pragma once

#include <functional>
#include <thread>
#include <memory>
#include <queue>
#include <mutex>

class ActiveObject
{
public:
    typedef std::function<void()> Message_t;
private:
    bool m_IsDone;
    std::unique_ptr<std::thread> m_spThread;
    std::queue<Message_t> m_Messages;
    std::mutex m_Mutex;
public:
    ActiveObject();
    ~ActiveObject();
    void SendMessage(Message_t Msg);
};

ActiveObject.cpp

#include "ActiveObject.h"

ActiveObject::ActiveObject():
    m_IsDone(false)
{
    auto Run = [&]
    {
        while(!m_IsDone)
        {
            Message_t Msg = nullptr;
            {
                std::lock_guard<std::mutex> Lock(m_Mutex);
                if(!m_Messages.empty())
                {
                    Msg = m_Messages.front();
                    m_Messages.pop();
                }
            }
            if(Msg)
                Msg();
        }
    };
    m_spThread.reset(new std::thread(Run));
}
  
ActiveObject::~ActiveObject()
{
    SendMessage([&](){m_IsDone = true;});
    m_spThread->join();
}
  
void ActiveObject::SendMessage(Message_t Msg)
{
    std::lock_guard<std::mutex> Lock(m_Mutex);
    m_Messages.push(Msg);
}

Применение:

#include <iostream>
#include "ActiveObject.h"

int main()
{
   ActiveObject Object;
   Object.SendMessage([]
   {
       for(size_t i = 0; i < 3000000; ++i)
           ;
       std::cout << "Lambda finished" << std::endl;
   });
   for(size_t i = 0; i < 3000000; ++i)
           ;
       std::cout << "Main finished" << std::endl;
   return 0;
}

 

Пример использования лямбды с алгоритмами stl. Несколько алгоритмов, чтобы показать простоту использования lambda функций в алгоритмах:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iterator>

struct Employee
{
    std::string FirstName;
    std::string SecondName;
    size_t Salary;
    Employee(const std::string& Name, const std::string& Surname, 
        size_t xSalary):
        FirstName(Name),
        SecondName(Surname),
        Salary(xSalary)
        {}
};

int main()
{
    std::vector<Employee> List;
    List.push_back(Employee("Kuz'ma", "Minin", 100));
    List.push_back(Employee("Valentin", "Pikul'", 200));
    List.push_back(Employee("Alexander", "Pushkin'", 222));
    List.push_back(Employee("Lermontov", "Michail'", 50));
    std::for_each(std::begin(List), std::end(List), 
        [&](const Employee& xEmploee)
    {
        std::cout << xEmploee.FirstName << " " << xEmploee.SecondName <<
            " has salary: " << xEmploee.Salary << "\n";
    });
    size_t OverallCost = std::accumulate(std::begin(List), std::end(List), 
        0, [&](size_t Summand, const Employee& xEmploee)
    {
        return Summand + xEmploee.Salary;
    });
    std::cout << "Monthly payment is " << OverallCost << "\n";
    std::vector<Employee> HonorList;
    std::transform(std::begin(List), std::end(List), 
        std::back_inserter(HonorList),
        [&](const Employee& xEmployee) -> Employee
    {
        Employee HonoredEmployee(xEmployee);
        HonoredEmployee.FirstName = std::string("Mr. ") + 
            HonoredEmployee.FirstName;
        return HonoredEmployee;
    });
    std::cout << "Honored people:\n";
    std::for_each(std::begin(HonorList), std::end(HonorList),
        [&](const Employee& xEmploee)
    {
        std::cout << xEmploee.FirstName << " " <<  xEmploee.SecondName 
            << "\n";
    });
    return 0;
}

В примере выше, вы можете видеть восставший из пепла алгоритм std::for_each. Нет, правда, вы часто его использовали, когда для него нужно было писать отдельную функцию или функтор? Я вот ни разу его не использовал до появления lambda. Сейчас же, его можно использовать как замену циклу for, что может сделать код более лаконичным за-счет концентрирования внимания на теле lambda, без оглядки на условия цикла. Герб Саттер говорил, что при использовании std::for_each может быть выполнена некая оптимизация(размотка цикла), что является еще одним плюсом в копилку этого алгоритма. Я не проверял этого, но считаю нужным об этом упомянуть.

Пример с обратным вызовом(callback)

#include <iostream>
#include <unordered_map>
#include <functional>
#include <string>

struct Message
{
    enum class MsgType{eHandshake, eProgress, eEnd} Type;
    std::string Data;
};

int main()
{
    std::unordered_map<Message::MsgType, 
		std::function<void(const Message&)>> List;
    List[Message::MsgType::eHandshake] = [](const Message& Msg)
    {
        std::cout << "Handle the handshake message: " << Msg.Data << "\n";
    };
    List[Message::MsgType::eProgress] = [](const Message& Msg)
    {
        std::cout << "Handle the progress message: " << Msg.Data << "\n";
    };
    List[Message::MsgType::eEnd] = [](const Message& Msg)
    {
        std::cout << "Handle the end message: " << Msg.Data << "\n";
    };

    Message HandshakeMsg = {Message::MsgType::eHandshake, "Hello machine!"};
    Message ProgressMsg = {Message::MsgType::eProgress,	"I'm still processing!"};
    Message EndMsg = {Message::MsgType::eEnd, "That's it!"};

    List[HandshakeMsg.Type](HandshakeMsg);
    List[ProgressMsg.Type](ProgressMsg);
    List[EndMsg.Type](EndMsg);

    return 0;
}

Я думаю, что вышеприведенные примеры хорошо показывают всю мощь lambda функций. Нет более нужды в написании функторов или отдельных функций, только для того, чтобы воспользоваться тем или иным алгоритмом stl. Я не знаю как вас, но меня это зачастую останавливало, т.к. для написания функции нужно было отключиться от текущего контекста кода, плюс засорять код определением функций, которые по сути не нужны вне данной функции. Используйте lambda функции в своем коде, это сделает его чище.