На днях, гуляя по багтрекеру gcc наткнулся на интересный баг, в нем используется сразу несколько возможностей C++11:
Анализируя этот баг, я подумал, что теперь можно удобно реализовать методы как first class citizens
Собственно, википедия объясняет нам, что такое first class citizens — это некая сущность, которая может быть создана в процессе работы программы, передана как параметр, присвоена переменной, может быть результатом работы функции.
Так как под рукой у меня не было свежего gcc или msvc, я решил собрать свежий clang-3.1:
Также я решил собрать библиотеку libcxx для использования всех возможностей нового компилятора:
Несколько слов о сборке libcxx: я решил взять последнюю версию из trunk, так как последний релиз не захотел у меня собираться (разбираться не хотелось, поэтому взял trunk). Также libcxx должна собираться с помощью clang, для этого я ставлю переменные окружения CC и CXX для замены компилятора на clang. Также у меня почему-то не захотели запускаться тесты (make check-libcxx)
Соответственно, для cmake переопределяем переменные окружения CC и CXX аналогично, как при сборке libcxx.
Итак, подготовительный процесс закончен, переходим к примеру:
Вывод программы:
На самом деле, аналогичную функциональность можно реализовать и без c++11, но будет это выглядеть менее читабельно. Основной вклад в читабельность кода вносит non-static member initialisation — мы получаем декларацию и реализацию метода аналогичную обычным методам в C++-03. Остальные возможности более-менее эмулируются средствами C++-03 и сторонними библиотеками: boost::function, boost::lambda.
Рассмотрим подробнее, что мы можем делать с такими объектами:
Здесь все просто, метод не является статическим, если он имеет доступ к this. Соответственно, при определении лямбда функции в теле класса, мы добавляем в capture list this. Теперь из лямбда функции мы можем обращаться ко всем членам класса (в том числе и приватным).
Здесь есть она особенность: на самом деле, здесь не совсем корректно используется понятие статических функций, так как изначально в C++ они определяются как функции, которые можно вызывать без созданного объекта, здесь мы все же вынуждены создать объект, чтобы дос��учаться до функции.
Как нестатическую определить функцию мы разобрались, теперь осталось понять, как это сделать вне класса, очень просто — необходимо в capture list передать по ссылке объект, к которому прицепляется данная функция:
Здесь мы должны соблюдать аккуратность при передаче ссылки на объект в capture list, так как операция определения функции и привязки ее к объекту разнесена по времени, можно допустить следующую ошибку:
«Привязать не к тому объекту, который указан в capture list».
Также, еще одно ограничение которое здесь присутствует, если мы прицепляем функцию вне декларации класса, то мы теряем доступ к приватным переменным класса:
При этом компилятор ругается:
Здесь все просто, так как метод является обычным членом класса, то добавив const к его описанию, мы как раз получаем то что нужно:
Компилятор ругает нас:
У честных C++ методов есть возможность определения, что метод не меняет членов класса, и его можно применять к константному объекту, такие методы помечаются квалификатором const. В примере — это метод get_x.
Если мы реализуем методы, как объекты, то такая возможность пропадает, вместо этого мы можем менять члены у константного объекта:
Если раскомментировать последний вызов, то компилятор ругается следующим образом:
Скорее всего присходит следующая последовательность действий:
non static member initialisation не более чем синтаксический сахар, и поэтому захват this в capture list происходит в конструкторе, а в конструкторе this имеет тип MutableFirstClass * const, и поэтому мы можем менять значения переменных.
Насколько я помню, в константных объектах менять значения членов — UB (кроме членов, помеченных квалификатором mutable), поэтому необходимо осторожно использовать такие методы в константных объектах.
На самом деле, возможность применения этого функционала довольно спорна — с одной стороны, мы легко можем реализовать паттерн «Декоратор» почти как в питоне, и это одна из сильных сторон: мы избавляемся от утомительной реализации кучи классов наследников, как в GoF. Также мы можем декорировать каждый объект индивидуальным способом: например, мы можем написать функцию decorate, которая получает на вход объект, и добавляет декоратор к одному из методов. Такое невозможно сделать, используя данный паттерн так, как он описан в GoF.
С другой стороны, отсутствие защиты членов константных объектов — это очень серьезный недостаток, поэтому необходимо серьезно подумать перед применением данного решения.
Также возрастает потребление памяти, в имплементации от libcxx каждый такой метод занимает 16 байт, таким образом, с увеличением количества методов, мы будем получать все более жирные объекты.
Также следует провести замеры времени и сравнить скорость вызова таких методов по сравнению с нативными методами C++ (можно сравнить скорость с виртуальными методами).
- std::function — механизм для создания функторов — объектов функций
- non static member initialisation — механизм для инициализации членов класса вне конструктора
- lambda — тут и так все ясно. Исчерпывающие статьи были здесь.
Анализируя этот баг, я подумал, что теперь можно удобно реализовать методы как first class citizens
Собственно, википедия объясняет нам, что такое first class citizens — это некая сущность, которая может быть создана в процессе работы программы, передана как параметр, присвоена переменной, может быть результатом работы функции.
Подготовка
Выбираем компилятор
Так как под рукой у меня не было свежего gcc или msvc, я решил собрать свежий clang-3.1:
mkdir llvm cd llvm svn co http://llvm.org/svn/llvm-project/llvm/tags/RELEASE_31/final ./ cd tools svn co http://llvm.org/svn/llvm-project/cfe/tags/RELEASE_31/final clang cd ../../ mkdir build cd build cmake ../llvm -DCMAKE_INSTALL_PREFIX=/home/pixel/fakeroot -DCMAKE_BUILD_TYPE=Release make -j4 make check-all make install
Выбираем библиотеку libcxx
Также я решил собрать библиотеку libcxx для использования всех возможностей нового компилятора:
mkdir libcxx cd libcxx svn co http://llvm.org/svn/llvm-project/libcxx/trunk ./ cd ../ mkdir build_libcxx cd build_libcxx CC=clang CXX=clang++ cmake ../libcxx -DCMAKE_INSTALL_PREFIX=/home/pixel/fakeroot -DCMAKE_BUILD_TYPE=Release make -j4 make install
Несколько слов о сборке libcxx: я решил взять последнюю версию из trunk, так как последний релиз не захотел у меня собираться (разбираться не хотелось, поэтому взял trunk). Также libcxx должна собираться с помощью clang, для этого я ставлю переменные окружения CC и CXX для замены компилятора на clang. Также у меня почему-то не захотели запускаться тесты (make check-libcxx)
Пример CMakeLists.txt для использования свежесобранного clang и libcxx
cmake_minimum_required(VERSION 2.8) project (clang_haxxs) add_definitions(-std=c++11 -nostdinc++) include_directories(/home/pixel/fakeroot/lib/clang/3.1/include) include_directories(/home/pixel/fakeroot/include/c++/v1) link_directories(/home/pixel/fakeroot/lib) add_executable(clang_haxxs main.cpp) set_target_properties(clang_haxxs PROPERTIES LINK_FLAGS -stdlib=libc++)
Соответственно, для cmake переопределяем переменные окружения CC и CXX аналогично, как при сборке libcxx.
Поясняющий пример
Итак, подготовительный процесс закончен, переходим к примеру:
#include <iostream> #include <functional> using namespace std; struct FirstClass { FirstClass(): x(0) { } int get_x() const { return x; } function<int ()> f1 = [this]() -> int { cout << "called member function f1..." << endl; ++x; f1 = f2; return 5; }; private: function<int ()> f2 = [this]() -> int { cout << "called member function f2..." << endl; return x; }; int x; }; int main() { FirstClass m; m.f1(); m.f1(); function<int ()> f3 = []() -> int { cout << "called free function f3..." << endl; return 100500; }; m.f1 = f3; m.f1(); return 0; }
Вывод программы:
called member function f1...
called member function f2...
called free function f3...На самом деле, аналогичную функциональность можно реализовать и без c++11, но будет это выглядеть менее читабельно. Основной вклад в читабельность кода вносит non-static member initialisation — мы получаем декларацию и реализацию метода аналогичную обычным методам в C++-03. Остальные возможности более-менее эмулируются средствами C++-03 и сторонними библиотеками: boost::function, boost::lambda.
Погружение
Рассмотрим подробнее, что мы можем делать с такими объектами:
Эмуляция статических и нестатических методов
Здесь все просто, метод не является статическим, если он имеет доступ к this. Соответственно, при определении лямбда функции в теле класса, мы добавляем в capture list this. Теперь из лямбда функции мы можем обращаться ко всем членам класса (в том числе и приватным).
Здесь есть она особенность: на самом деле, здесь не совсем корректно используется понятие статических функций, так как изначально в C++ они определяются как функции, которые можно вызывать без созданного объекта, здесь мы все же вынуждены создать объект, чтобы дос��учаться до функции.
Настройка методов вне класса
Как нестатическую определить функцию мы разобрались, теперь осталось понять, как это сделать вне класса, очень просто — необходимо в capture list передать по ссылке объект, к которому прицепляется данная функция:
function<int ()> f4 = [&m]() ->int { cout << "called free function f4 with capture list..." << endl; return m.get_x() + 1; }; m.f1 = f4; m.f1();
Здесь мы должны соблюдать аккуратность при передаче ссылки на объект в capture list, так как операция определения функции и привязки ее к объекту разнесена по времени, можно допустить следующую ошибку:
«Привязать не к тому объекту, который указан в capture list».
Также, еще одно ограничение которое здесь присутствует, если мы прицепляем функцию вне декларации класса, то мы теряем доступ к приватным переменным класса:
function<int ()> err = [&m]() ->int { cout << "called free function err with capture list..." << endl; return m.x + 1; };
При этом компилятор ругается:
/usr/src/projects/clang/usage/main.cpp:64:12: error: 'x' is a private member of 'FirstClass'Запрещение переопределения метода
Здесь все просто, так как метод является обычным членом класса, то добавив const к его описанию, мы как раз получаем то что нужно:
struct FirstClassConst { const function <int()> f1 = []() -> int { return 1; }; }; FirstClassConst mc; mc.f1 = f3;
Компилятор ругает нас:
/usr/src/projects/clang/usage/main.cpp:70:8: error: no viable overloaded '='
mc.f1 = f3;
~~~~~ ^ ~~Отсутствие const методов
У честных C++ методов есть возможность определения, что метод не меняет членов класса, и его можно применять к константному объекту, такие методы помечаются квалификатором const. В примере — это метод get_x.
Если мы реализуем методы, как объекты, то такая возможность пропадает, вместо этого мы можем менять члены у константного объекта:
struct MutableFirstClass { int x; MutableFirstClass(): x(0){} int nonConstMethod() { ++x; return x; } function <int()> f1 = [this]() -> int { this->x = 100500; return x; }; }; const MutableFirstClass mm; mm.f1(); //mm.nonConstMethod();
Если раскомментировать последний вызов, то компилятор ругается следующим образом:
/usr/src/projects/clang/usage/main.cpp:93:2: error: member function 'nonConstMethod' not viable: 'this' argument has type 'const MutableFirstClass', but function is not marked const
mm.nonConstMethod();
^~Скорее всего присходит следующая последовательность действий:
non static member initialisation не более чем синтаксический сахар, и поэтому захват this в capture list происходит в конструкторе, а в конструкторе this имеет тип MutableFirstClass * const, и поэтому мы можем менять значения переменных.
Насколько я помню, в константных объектах менять значения членов — UB (кроме членов, помеченных квалификатором mutable), поэтому необходимо осторожно использовать такие методы в константных объектах.
Что дальше
На самом деле, возможность применения этого функционала довольно спорна — с одной стороны, мы легко можем реализовать паттерн «Декоратор» почти как в питоне, и это одна из сильных сторон: мы избавляемся от утомительной реализации кучи классов наследников, как в GoF. Также мы можем декорировать каждый объект индивидуальным способом: например, мы можем написать функцию decorate, которая получает на вход объект, и добавляет декоратор к одному из методов. Такое невозможно сделать, используя данный паттерн так, как он описан в GoF.
С другой стороны, отсутствие защиты членов константных объектов — это очень серьезный недостаток, поэтому необходимо серьезно подумать перед применением данного решения.
Также возрастает потребление памяти, в имплементации от libcxx каждый такой метод занимает 16 байт, таким образом, с увеличением количества методов, мы будем получать все более жирные объекты.
Также следует провести замеры времени и сравнить скорость вызова таких методов по сравнению с нативными методами C++ (можно сравнить скорость с виртуальными методами).