Методы, как first class citizens в C++

в 12:09, , рубрики: c++, c++11, ненормальное программирование, метки: ,

На днях, гуляя по багтрекеру gcc наткнулся на интересный баг, в нем используется сразу несколько возможностей C++11:

Анализируя этот баг, я подумал, что теперь можно удобно реализовать методы как 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 (кроме членов, помеченных квалификатором volatile), поэтому необходимо осторожно использовать такие методы в константных объектах.

Что дальше

На самом деле, возможность применения этого функционала довольно спорна — с одной стороны, мы легко можем реализовать паттерн «Декоратор» почти как в питоне, и это одна из сильных сторон: мы избавляемся от утомительной реализации кучи классов наследников, как в GoF. Также мы можем декорировать каждый объект индивидуальным способом: например, мы можем написать функцию decorate, которая получает на вход объект, и добавляет декоратор к одному из методов. Такое невозможно сделать, используя данный паттерн так, как он описан в GoF.

С другой стороны, отсутствие защиты членов константных объектов — это очень серьезный недостаток, поэтому необходимо серьезно подумать перед применением данного решения.

Также возрастает потребление памяти, в имплементации от libcxx каждый такой метод занимает 16 байт, таким образом, с увеличением количества методов, мы будем получать все более жирные объекты.

Также следует провести замеры времени и сравнить скорость вызова таких методов по сравнению с нативными методами C++ (можно сравнить скорость с виртуальными методами).

Автор: prograholic

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js