Luke Skywalker on new Jedi techniques – Lambda-выражения – Lambdas as closures



Luke Skywalker on new Jedi techniques – Lambda-выражения – Lambdas as closures

0 0


cpp11slides

Slides on C++11

On Github Dahko / cpp11slides

Luke Skywalker on new Jedi techniques

Scott Mayers on new C++11/C++14

Lambda-выражения

Lambda expressions basics

Быстрый способ создать функцию в точке ее использования

std::vector<int> v;
…
auto it = std::find_if(v.cbegin(), v.cend(),
                       [](int i) { return i > 0 && i < 10; });

Генерирует примерно следующее:

class MagicType1 {
public:
bool operator()(int i) const { return i > 0 && i < 10; }
};
auto it = std::find_if(v.cbegin(), v.cend(), MagicType1());

Lambda expressions basics

Быстрый способ создать функцию в точке ее использования

std::map<Solution*, int> mapSolID;
...
int someSolId = ...;
...
itInt = std::find_if(
    mapSolID.begin(), mapSolID.end(), ???
);
class IsEqualSolID
{
public:
    int m_nTarget;
    IsEqualSolID(int nTarget):m_nTarget(nTarget) {}
    bool operator ()(const std::pair<Solution*, int>& a)
    {
        return a.second == m_nTarget;
    }
};

Lambda expressions basics

Быстрый способ создать функцию в точке ее использования

std::map<Solution*, int> mapSolID;
...
int someSolId = ...;
...
itInt = std::find_if(
    mapSolID.begin(), mapSolID.end(), IsEqualSolID(someSolId))
);
class IsEqualSolID
{
public:
    int m_nTarget;
    IsEqualSolID(int nTarget):m_nTarget(nTarget) {}
    bool operator ()(const std::pair<Solution*, int>& a)
    {
        return a.second == m_nTarget;
    }
};

Lambda expressions basics

Быстрый способ создать функцию в точке ее использования

std::map<Solution*, int> mapSolID;
...
int someSolId = ...;
...
itInt = std::find_if(mapSolID.begin(), mapSolID.end(), 
      [&someSolId](const std::pair<Solution*, int>& a) {
	  return a.second = someSolId;
      }
);

Lambdas as closures

Lambdas as closures

Функциональные объекты, задаваемые лямбдами, называются замыканиями (closures). Замыкания могут существовать вне исходногно контекста (scope):

std::function<bool(int)> returnClosure(int a) {
    int b, c;
    …
    return [](int x) { int a*x*x + b*x + c == 0; };
}

auto f = returnClosure(10);

Вот тут

if(f(22) == true) { //... }

чему равны a, b, c? Му же уже вышли из returnClosure!

Lambdas as closures

Со статическими и глобальными переменными все "нормально":

int a; 
std::function<bool(int)> returnClosure() {
    static int b, c;
    …
    return [](int x) { int a*x*x + b*x + c == 0; };
}
  auto f = returnClosure(10);

А локльные переменные надо специально захватывать (capture).

Capturing local variables

Захваченная переменная копируется в closure. Следующий код

{
    int minVal;
    double maxVal;
   …
    auto it = std::find_if(v.cbegin(), v.cend(),
                          [minVal, maxVal](int i)
                          { return i > minVal && i < maxVal; }
    );
}

геренирует

class MagicType {
public:
    MagicType(int v1, double v2): _minVal(v1), _maxVal(v2) {}
    bool operator()(int i) const { return i > _minVal && i > _maxVal; }
private:
    int _minVal;
    double _maxVal;
};
auto it = std::find_if(v.cbegin(), v.cend(), MagicType(minVal, maxVal));

Capturing local variables

Захваченная переменная копируется в closure. Следующий код

{
    int minVal;
    double maxVal;
   …
    auto it = std::find_if(v.cbegin(), v.cend(),
                          [&minVal, &maxVal](int i)
                          { return i > minVal && i < maxVal; }
    );
}

геренирует

class MagicType {
public:
    MagicType(int& v1, double& v2): _minVal(v1), _maxVal(v2) {}
    bool operator()(int i) const { return i > _minVal && i > _maxVal; }
private:
    int& _minVal;
    double& _maxVal;
};
auto it = std::find_if(v.cbegin(), v.cend(), MagicType(minVal, maxVal));

Capturing local variables

Можно прописать захват всех переменных по умолчанию:

auto it = std::find_if( v.cbegin(), v.cend(),   // default is
                      [=](int i)                // by value
                      { return i > minVal && i < maxVal; });
auto it = std::find_if( v.cbegin(), v.cend(),   // default is
                      [&](int i)                // by ref
                      { return i > minVal && i < maxVal; });

При этом можно указать другой способ захвата для отдельных переменных:

auto it = std::find_if(v.cbegin(), v.cend(),
                       [=, &maxVal](int i) 
                       { return i > minVal &&      // minVal by value
                                i < maxVal; });    // maxVal by reference

Lambda Return Types

Тип возвращаемого значания указывать не нужно, если:

  • лямбда возвращает void;
  • тело лямбды представляет собой “return expr;”
    • (тогда возвращаемым типом считается тип expr).

Иначе надо указывать явно через "trailing return type syntax":

std::vector<double> v;
…
std::transform(v.begin(), v.end(), v.begin(),
              [](double d) -> double
              {
                  makeLogEntry("std::transform", d);
                  return std::sqrt(std::abs(d));
              }
);

C++14

Возвращаемы тип вычисляется автоматически (для всех функций). Аргументы лямбд тоже вычисляются.

auto magicValue1(int seed) // both returns are int
{
    if (seed >= 0) return seed;
    else return 42;
}
std::partition( d.begin(), d.end(),
                [] (auto val) // ->bool not required, val type automatic
                { if (val % 10 == 0) return true;
                  return false; }
);

Init captures (захват через инициализацию)

return [ minVal = computeMinVal(this->minSeed) ] (int x)
       { return minVal <= x };

Тонкости

  • Захват членов класса (надо захватывать this, по значению невозможно)
  • У лямбды без параметров скобки можно опустить: []{ //body }
  • Лямбды могут все, что и обычные функции. Но Скотт рекомендует не увлекаться и писать короткие, понятные лямбды, вытекающие из контекста.
  • Лямбды по умолчанию константны, но бывают mutable lambdas.

std::function

Storing lambdas

Ранее мы рассматривали простые примеры, когда лямбда создается и сразу же используется в алгоритме stl.

std::vector<int> v;
…
auto it = std::find_if(v.cbegin(), v.cend(),
                       [](int i) { return i > 0 && i < 10; });

Когда мы обсуждали захват переменных, было понятно, что захват по значению нужен именно для отложенного запуска лямбд.

Как же хранить лямбды?

auto

??? fn = [](MyClass* x) { return x->IsGood(); }
auto fn = [](MyClass* x) { return x->IsGood(); }
"Let's make it available everywhere!"
auto x1 = 10;        // x1: int
std::map<int, std::string> m;
auto i1 = m.begin(); // i1: std::map<int, std::string>::iterator
for(auto i = m.begin(); i!=m.end(); i++) { i->second+="!"; }

"auto" переменные имеют тип инициализирующего их выражения.

Можно добавлять const/volatile и reference/pointer:

const auto *x2 = &x1; // x2: const int*
const auto& i2 = m;   // i2: const std::map<int, std::string>&
auto ci = m.cbegin(); // ci: std::map<int, std::string>::const_iterator

Выведение типа аналогично выведению типа в шаблонах.

std::function

std::function<int(std::string&)> f;     // f refers to callable entity
                                        // compatible with given sig.
int someFunc(std::string&);             // some function
f = someFunc;                           // f refers to someFunc
f = [](std::string &s)->unsigned
    { s += "!"; return s.size(); };     // f refers to a closure
class Gadget {
public:
int operator()(std::string&);           // function call operator
…
};
Gadget g;
f = g;                                  // f refers to g

std::function - универсальное представление вызываемого объекта(callable entity)

std::function

bool myFn(int n, MyClass* obj) { //... }

std::function<bool(int, MyClass*)> fn = myFn;
bool myFn1(std::string str) { //... }
void myFn2(const char* str) { //... }

std::function<void(std::string)> fn = myFn1;
                                 fn = myFn2;

std::function - использование в callback'ах

class Button: public SomeGUIFrameworkBaseClass {
public:
…
    using CallbackType = std::function<void(short)>;
    void setCallback(const CallbackType& cb)
    {
        clickHandler = cb;
    }
    virtual void onClick(short upOrDown) // invoked by base class
    {
        clickHandler(upOrDown); // invoke function object
    }
private:
    CallbackType clickHandler;
};
Button b;
b.setCallback([](int v) { logClick(v); });

std::function - пример из жизни

В Palign.dll был класс CSimplex, делающий минимизацию по многим переменным. Он принимал на вход функцию, которую нужно было минимизировать, и варьировал ее аргументы до достижения минимума.

typedef double (*FuncToMinimize) (double*);
class CSimplex  
{
public:
    CSimplex(FuncToMinimize, int, double*, double, bool _bound = false, ...);
    ...
};

В какой-то момент понадобилось передавать туда не функцию, а функтор, имеющий ссылки на другие объекты. В указатель на функцию такое в принципе не передашь - разве только вводить глобальные объекты.

public:
    CSimplex(std::function<double(double*)>, int, double*, double, bool _bound = false, ...);

Альтернативное решение - передавать указатель на функтор IFuncToMinimize с функцией virtual double operator() (double*)

Лямбды в реальной жизни

Паттерн "Command"

Хранение генерирующих функций фабрики

class CReflectSeriazableFactoryFace {
public:
    CReflectSeriazableFactoryFace();
    virtual ISerializableFace* MakeSerializableObject( 
                               GUID const& serialId );
    virtual unsigned ObjectTypesCount() { return m_map.size(); }
    virtual GUID GetObjectGUID(unsigned i);
protected:
    std::map<GUID, std::function<IDocumentNode*()>> m_map;
}
CReflectSeriazableFactoryFace::CReflectSeriazableFactoryFace() {
    m_map[SERID_IPOLYHEDRONNode] = []{ return new CModelNodeImpl; };
    m_map[SERID_IReflectDataNode] = []{ return new CReflectDataNode; };
}
ISerializableFace* CReflectSeriazableFactoryFace::MakeSerializableObject( 
                                                 GUID const& serialId ) {
    auto creator = m_map.find(serialId);
    if(creator == m_map.end()) return NULL;

    IDocumentNode *node = creator->second();
    if(!node) return NULL;

    CDocumentNodeCommonSerializer * wrapper =
                new CDocumentNodeCommonSerializer(node);
    return wrapper;
}

Паттерн "Command"

Callback после чтения объекта сериализатором из файла

new CPolyhedronSer(m_phModel, 
                   [this](POLYHEDRON* model) {
                       this->SetPOLYHEDRONByCloning(model); 
                       delete model;
                   } 
);

Функциональное программирование

std::map<int, std::string> m;
typedef m::value_type MyMapVal;

for(auto i = m.begin(); i!=m.end(); i++) { i->second+="!"; }
std::foreach(m.begin(), m.end(), [](MyMapVal & i) { i.second+="!"; }

Чем второй вариант може быть лучше первого?

В случае с лямбдой мы работаем не с итератором, а со значением, так что не можем "испортить" итератор.

for(auto i = m.begin(); i!=m.end(); i++) { i->second+="!"; i++; }
std::foreach(m.begin(), m.end(), [](MyMapVal & i) { i.second+="!"; /*i++;*/ }

Функциональный стиль - максимально ограничиваем множество доступных коду данных, чтобы минимизировать побочные эффекты. В идеале вообще без переменных. Такой код надежнее и легче тестируется.

Коду внутри лямбд доступны только их параметры и захваченные переменные (поэтому лучше избегать [&] и [=]).

Использование в STL-алгоритмах

Примеров испльзования лямбд в STL-алгоритмах выше было множество.

С приходом лямбд можно расширить количество алгоритмов, которые мы повседневно используем, а не писать циклы вручную.

  • for_each / transform
  • find_if / count_if
  • remove_if / copy_if
  • generate
  • accumulate
  • partition
  • max_element / min_element

Haskell

Неплохой язык для знакомства с функциональный программированием

Ресурсы:

Инициализация константных переменных

Переменная не должна меняться по смыслу, но ее иницаилизация не укладывается в a = expr.

const auto sortedInts = []()->std::vector<int> { // init const vector
    std::vector<int> v(NUM_VALUES); // w/NUM_VALUES
    std::iota(v.begin(), v.end(), 0-NUM_VALUES/2); // sequential ints
    return v; // centered at 0
}();

const auto priority = [=]()->Priority {
    makeLogEntry("Initializing priority");
    auto cInfo = getCustomerInfo(customerID);
    Priority p = (cInfo.salesInLast12Months() < bonusThreshhold)
    ? normalPriority
    : highPriority;
    return p;
}();

Это тоже трюк из функционального программирования - там по-другому нельзя.

override

override

class IDocumentNode : public IOctInterface {
public:
    // List of sub-nodes
    virtual unsigned ChildCount() const = 0;
    virtual IDocumentNode* Child(unsigned i) = 0;
    virtual void AddChild(IDocumentNode * pChild) = 0;
	...
};
class TDocumentNodeLeaf : public IDocumentNode {
public:
  virtual size_t ChildCount() const { return 0;} ;
  virtual IDocumentNode* Child(size_t i) { return NULL; };
  virtual void AddChild(IDocumentNode *) { return; }
  ...
};

class TDocumentNodeFolder : public TDocumentNodeLeaf {
public:
  virtual size_t ChaildCount() const { return m_vChilds.size(); } 
  virtual IDocumentNode* Child(int i) { 
      return m_vChilds[i];
    };
  ...
}
TDocumentNodeFolder obj;
obj.AddChild(something);
std::cout << obj.ChildCount();	// prints 0 !!!

override

class IDocumentNode : public IOctInterface {
public:
    // List of sub-nodes
    virtual unsigned ChildCount() const = 0;
    virtual IDocumentNode* Child(unsigned i) = 0;
    virtual void AddChild(IDocumentNode * pChild) = 0;
	...
};
class TDocumentNodeLeaf : public IDocumentNode {
public:
  virtual size_t ChildCount() const override { return 0;} ;
  virtual IDocumentNode* Child(size_t i) override { return NULL; };
  virtual void AddChild(IDocumentNode *) override { return; }
  ...
};

class TDocumentNodeFolder : public TDocumentNodeLeaf {
public:
  virtual size_t ChaildCount() const override { return m_vChilds.size(); } 
  virtual IDocumentNode* Child(int i) override { 
      return m_vChilds[i];
    };
  ...
}

error C3668: 'TDocumentNodeFolder::ChaildCount' : method with override specifier 'override' did not override any base class methods

Немного статистики

Использование std:: в проектах

С учетом vector Без vector 2647 OxygenCustomReports 1089 OxygenCustomReports 695 Models 339 Reflect 663 oxygeninclusionselectionplugin 271 Oxygen 654 Reflect 262 oxygeninclusionselectionplugin 645 GradingGIADll 182 GradingGIADll 554 Oxygen 150 OxygenHWS 258 OxygenHWS 145 Models 134 Palign 70 Palign 87 OHWS_Reflect2 59 OHWS_Reflect2 57 Presentation 52 Presentation 42 HPOxygenGUI 27 HPOxygenGUI 35 TestPanel 21 TestPanel 20 OxygenBatch 13 OxygenInterface 15 OxygenInterface 5 SceneInfo 7 OxExtScript 5 OxExtScript 6 SceneInfo 3 OctInterface

Использование отдельных слов из std::

4379 vector 37 function 16 multimap 7 forward 3 tstring 2 partition 1510 string 33 swap 16 binary 7 allocator 3 search 2 log 314 map 31 for 14 bidirectional 6 lower 3 reverse 2 hex 212 pair 30 auto 12 valarray 6 istreambuf 3 less 2 greater 101 find 29 ios 12 transform 6 boolalpha 3 cout 2 foreach 93 ostream 29 ifstream 11 setprecision 6 3 abs 2 floor 83 make 26 max 11 memmove 5 unique 2 ws 2 ctype 82 istream 25 char 11 advance 5 unary 2 use 2 count 74 sort 24 ofstream 10 shared 5 streamsize 2 type 2 complex 56 set 24 exception 10 istringstream 5 bind 2 strlen 2 collate 52 tr 21 endl 10 fixed 4 sin 2 stable 2 accumulate 52 numeric 19 remove 10 fill 4 PathString 2 sqrt 1 wstringstream 50 list 19 min 9 ostringstream 4 memcpy 2 runtime 1 wstreambuf 44 copy 19 back 9 bad 4 deque 2 rotate 1 wostringstream 44 basic 18 wstring 8 getline 4 cos 2 replace 1 wostream 42 stringstream 18 fabs 7 to 4 atan 2 queue 1 wofstream

Использование лямбд

86 Reflect 29 HPOxygenGUI 25 OxygenCustomReports 5 oxygeninclusionselectionplugin 5 OxExtScript 5 Models 4 TestPanel 4 OxExtSript-share 4 OHWS_Reflect2 3 Oxygen 3 GradingGIADll 2 OxygenHWS

Продолжение следует...

0