Сообщение [trick] for_each_field от 16.01.2017 8:45
Изменено 16.01.2017 10:01 Evgeny.Panasyuk
[trick] for_each_field
for_each_field Proof-of-Concept
Пример обхода полей
В этом примере обходятся поля свежеопределенной структуры Gadget. Эти поля нигде кроме как в определении структуры не перечисляются.#include "for_each_field.hpp"
#include <iostream>
using namespace std;
using namespace proof_of_concept;
struct Gadget
{
field<int> x;
field<char> y;
field<double> z;
};
int main()
{
Gadget gadget{42, 'J', 3.1415926};
for_each_field(gadget, [](auto x)
{
cout << x << endl;
});
}
Вывод
42
J
3.14159
Live Demo
Детали реализации
Нам требуется каким-то образом опросить все поля, причём не называя их по имени. Трюк заключается в вытаскивании данных о полях через список инициализации агрегата.Например объект типа struct Pair {int x; double y;}; можно сконструировать следующим образом: Pair{42, 3.1415926} — не объявляя при этом специального конструктора. Как видно здесь имеется как минимум односторонняя тропинка — мы передаём число 42, которое отправляется во внутрь структуры инициализировать поле Pair::x, при этом не называя его по имени.
Для организации обратной связи от поля будем использовать обёртку field:
template<typename T>
struct field
{
template<typename U>
field(field_visitor<U> &&s);
field(T x);
T value;
};
Идея заключается в том, что агрегат вида struct Pair {field<int> x; field<double> y;}; можно инициализировать двумя способами:
- Pair{42, 3.1415926}; — для обычного использования.
Pair{field_visitor<F>{}, field_visitor<F>{}} — для извлечения информации о полях.
Всё что нам нужно от конкретного поля — узнать его this. Эту информацию поле сообщит нашему подсадному посетителю, который вломится через чёрный вход:
template<typename T> template<typename U>
field<T>::field(field_visitor<U> &&f)
{
f(this);
}
Очевидно что таким образом — из конструктора — напрямую прочитать информацию из ранее проинициализированного поля не получится, так как оно уже сконструировано до нас. Поэтому нам придётся идти другим путём.
Помимо this конкретного экземпляра поля, мы также можем получить адрес по которому находится объект самой структуры Pair. Зная эти два значения можно вычислить смещение поля относительно начала структуры Pair, то есть получается эдакий offsetof. После чего это смещение можно применить к другому(интересующему нас) объекту типа Pair.
Это всё конечно на грани
Теперь, для того чтобы обойти все поля, нам нужно узнать их количество — это осуществляется через SFINAE.
SFINAE проверка опирается на decltype и выглядит следующим образом:
...
template<typename U>
static auto check(int) ->
decltype(U{Xs{}...}, yes_type{});
...
Тут особенность в том, что список инициализирующий агрегат может содержать меньше значений чем всего в структуре полей, и оставшиеся поля в таком случае будут value-initialized. Поэтому нам нужно найти не просто такое количество значений при котором подстановка не фейлится, а границу количества при котором происходит фейл. Это реализуется например перебором от нуля значений и до
Сравнение с ручным вариантом
В ручном варианте (функция test_handwritten ниже) обход полей производится вручную:#include "for_each_field.hpp"
#include <vector>
using namespace proof_of_concept;
template<typename T>
void use_field(T &);
/****************************************************************/
struct Widget
{
field<int> x;
field<std::vector<int>> y;
field<char> z;
};
void test_handwritten(Widget &w)
{
use_field(w.x.value);
use_field(w.y.value);
use_field(w.z.value);
}
void test_for_each_field(Widget &w)
{
for_each_field(w, [](auto &x)
{
use_field(x);
});
}
Benchmark
Никакой benchmark не нужен, так как результирующий ассемблерный код в обоих вариантах идентиченСкрипт сравнения ассемблерного кода вариантов
g++ versus_handwritten.cpp -std=c++17 -O3 -Wall -pedantic -DNDEBUG -S -masm=intel &&
cat versus_handwritten.s | c++filt > versus_handwritten.filtered.s &&
sed -n '/test_for_each_field(Widget&):$/,/seh_endproc/p' versus_handwritten.filtered.s > for_each_field.s &&
sed -n '/test_handwritten(Widget&):$/,/seh_endproc/p' versus_handwritten.filtered.s > handwritten.s &&
diff -u handwritten.s for_each_field.s > result.diff &&
cat result.diff
ASM diff
--- handwritten.s 2017-01-16 11:43:08.199916500 +0300
+++ for_each_field.s 2017-01-16 11:43:08.171914900 +0300
@@ -1,5 +1,5 @@
-test_handwritten(Widget&):
-.LFB1456:
+test_for_each_field(Widget&):
+.LFB1457:
push rbx
.seh_pushreg rbx
sub rsp, 32
Live Demo
Конструктор по умолчанию
Как видно из сравнения, компилятор успешно выкинул все вспомогательные runtime вызовы и вычисления сдвигов. Но, тем не менее, относительно производительности есть значимый недостаток — при обходе полей посетителем посредством конструирования дополнительного объекта, также будут конструироваться поля оригинальных типов (field::value) посредством default initialization. Если у объекта тяжёлый конструктор по-умолчанию, то он будет вызван и тем самым повлияет на производительность.Но, к счастью, большинство конструкторов по умолчанию простые, и могут быть выкинуты компилятором — так как эти поля никак не используются — как например произошло в случае со std::vector выше (собственно для демонстрации этого он и был туда добавлен).
Как вариант можно добавить дополнительный шаблонный параметр в field и протаскивать его из внешней структуры, и в зависимости от его состояния глушить создание лишних полей, но это уже намного более громоздко для пользователя — лучше наверное взять BOOST_FUSION_DEFINE_STRUCT/BOOST_HANA_DEFINE_STRUCT, X Macros и прочие макросы высшего порядка.
Альтернативные возможности
Есть и другие варианты с помощью можно "пошевилить" поля не обращаясь к ним по имени, и они доступны даже для C++98. Например для этой же цели можно запрячь конструктор копирования или оператор присваивания сгенерированные компилятором — ведь там автоматически осуществляется поэлементное передёргивание полями. Но, в таком случае код посещения будет в обёртке field, и видимо придётся жонглировать через глобальный/TLS контекст, что уже крайне маловероятно поддастся оптимизатору.Файлы
for_each_field.hpp | |
| |
versus_handwritten.cpp | |
| |
for_each_field.s | |
| |
handwritten.s | |
| |
versus_handwritten.filtered.s | |
| |
[trick] for_each_field
EDIT: в комментариях указали на то, что похожую технику уже презентовал на CppCon 2016 Антон Полухин. Плюс есть дополнения от Bruno Dutra.
https://github.com/apolukhin/magic_get
http://apolukhin.github.io/magic_get/index.html
https://www.youtube.com/watch?v=abdeAew3gmQ
слайды
Например объект типа struct Pair {int x; double y;}; можно сконструировать следующим образом: Pair{42, 3.1415926} — не объявляя при этом специального конструктора. Как видно здесь имеется как минимум односторонняя тропинка — мы передаём число 42, которое отправляется во внутрь структуры инициализировать поле Pair::x, при этом не называя его по имени.
Для организации обратной связи от поля будем использовать обёртку field:
Идея заключается в том, что агрегат вида struct Pair {field<int> x; field<double> y;}; можно инициализировать двумя способами:
Всё что нам нужно от конкретного поля — узнать его this. Эту информацию поле сообщит нашему подсадному посетителю, который вломится через чёрный вход:
Очевидно что таким образом — из конструктора — напрямую прочитать информацию из ранее проинициализированного поля не получится, так как оно уже сконструировано до нас. Поэтому нам придётся идти другим путём.
Помимо this конкретного экземпляра поля, мы также можем получить адрес по которому находится объект самой структуры Pair. Зная эти два значения можно вычислить смещение поля относительно начала структуры Pair, то есть получается эдакий offsetof. После чего это смещение можно применить к другому(интересующему нас) объекту типа Pair.
Это всё конечно на гранифола UB, и нужно внимательно свериться со стандартом, выбрать правильные ограничения (standard layout type?). Но даже в случае если в каком-то из вариантов окажется UB — то на конкретных компиляторах это вполне может работать, необходимо лишь скрупулёзно протестировать.
Теперь, для того чтобы обойти все поля, нам нужно узнать их количество — это осуществляется через SFINAE.
SFINAE проверка опирается на decltype и выглядит следующим образом:
Тут особенность в том, что список инициализирующий агрегат может содержать меньше значений чем всего в структуре полей, и оставшиеся поля в таком случае будут value-initialized. Поэтому нам нужно найти не просто такое количество значений при котором подстановка не фейлится, а границу количества при котором происходит фейл. Это реализуется например перебором от нуля значений и дозабора первого фейла.
Но, к счастью, большинство конструкторов по умолчанию простые, и могут быть выкинуты компилятором — так как эти поля никак не используются — как например произошло в случае со std::vector выше (собственно для демонстрации этого он и был туда добавлен).
Как вариант можно добавить дополнительный шаблонный параметр в field и протаскивать его из внешней структуры, и в зависимости от его состояния глушить создание лишних полей, но это уже намного более громоздко для пользователя — лучше наверное взять BOOST_FUSION_DEFINE_STRUCT/BOOST_HANA_DEFINE_STRUCT, X Macros и прочие макросы высшего порядка.
https://github.com/apolukhin/magic_get
http://apolukhin.github.io/magic_get/index.html
https://www.youtube.com/watch?v=abdeAew3gmQ
слайды
for_each_field Proof-of-Concept
Пример обхода полей
В этом примере обходятся поля свежеопределенной структуры Gadget. Эти поля нигде кроме как в определении структуры не перечисляются.#include "for_each_field.hpp"
#include <iostream>
using namespace std;
using namespace proof_of_concept;
struct Gadget
{
field<int> x;
field<char> y;
field<double> z;
};
int main()
{
Gadget gadget{42, 'J', 3.1415926};
for_each_field(gadget, [](auto x)
{
cout << x << endl;
});
}
Вывод
42
J
3.14159
Live Demo
Детали реализации
Нам требуется каким-то образом опросить все поля, причём не называя их по имени. Трюк заключается в вытаскивании данных о полях через список инициализации агрегата.Например объект типа struct Pair {int x; double y;}; можно сконструировать следующим образом: Pair{42, 3.1415926} — не объявляя при этом специального конструктора. Как видно здесь имеется как минимум односторонняя тропинка — мы передаём число 42, которое отправляется во внутрь структуры инициализировать поле Pair::x, при этом не называя его по имени.
Для организации обратной связи от поля будем использовать обёртку field:
template<typename T>
struct field
{
template<typename U>
field(field_visitor<U> &&s);
field(T x);
T value;
};
Идея заключается в том, что агрегат вида struct Pair {field<int> x; field<double> y;}; можно инициализировать двумя способами:
- Pair{42, 3.1415926}; — для обычного использования.
Pair{field_visitor<F>{}, field_visitor<F>{}} — для извлечения информации о полях.
Всё что нам нужно от конкретного поля — узнать его this. Эту информацию поле сообщит нашему подсадному посетителю, который вломится через чёрный вход:
template<typename T> template<typename U>
field<T>::field(field_visitor<U> &&f)
{
f(this);
}
Очевидно что таким образом — из конструктора — напрямую прочитать информацию из ранее проинициализированного поля не получится, так как оно уже сконструировано до нас. Поэтому нам придётся идти другим путём.
Помимо this конкретного экземпляра поля, мы также можем получить адрес по которому находится объект самой структуры Pair. Зная эти два значения можно вычислить смещение поля относительно начала структуры Pair, то есть получается эдакий offsetof. После чего это смещение можно применить к другому(интересующему нас) объекту типа Pair.
Это всё конечно на грани
Теперь, для того чтобы обойти все поля, нам нужно узнать их количество — это осуществляется через SFINAE.
SFINAE проверка опирается на decltype и выглядит следующим образом:
...
template<typename U>
static auto check(int) ->
decltype(U{Xs{}...}, yes_type{});
...
Тут особенность в том, что список инициализирующий агрегат может содержать меньше значений чем всего в структуре полей, и оставшиеся поля в таком случае будут value-initialized. Поэтому нам нужно найти не просто такое количество значений при котором подстановка не фейлится, а границу количества при котором происходит фейл. Это реализуется например перебором от нуля значений и до
Сравнение с ручным вариантом
В ручном варианте (функция test_handwritten ниже) обход полей производится вручную:#include "for_each_field.hpp"
#include <vector>
using namespace proof_of_concept;
template<typename T>
void use_field(T &);
/****************************************************************/
struct Widget
{
field<int> x;
field<std::vector<int>> y;
field<char> z;
};
void test_handwritten(Widget &w)
{
use_field(w.x.value);
use_field(w.y.value);
use_field(w.z.value);
}
void test_for_each_field(Widget &w)
{
for_each_field(w, [](auto &x)
{
use_field(x);
});
}
Benchmark
Никакой benchmark не нужен, так как результирующий ассемблерный код в обоих вариантах идентиченСкрипт сравнения ассемблерного кода вариантов
g++ versus_handwritten.cpp -std=c++17 -O3 -Wall -pedantic -DNDEBUG -S -masm=intel &&
cat versus_handwritten.s | c++filt > versus_handwritten.filtered.s &&
sed -n '/test_for_each_field(Widget&):$/,/seh_endproc/p' versus_handwritten.filtered.s > for_each_field.s &&
sed -n '/test_handwritten(Widget&):$/,/seh_endproc/p' versus_handwritten.filtered.s > handwritten.s &&
diff -u handwritten.s for_each_field.s > result.diff &&
cat result.diff
ASM diff
--- handwritten.s 2017-01-16 11:43:08.199916500 +0300
+++ for_each_field.s 2017-01-16 11:43:08.171914900 +0300
@@ -1,5 +1,5 @@
-test_handwritten(Widget&):
-.LFB1456:
+test_for_each_field(Widget&):
+.LFB1457:
push rbx
.seh_pushreg rbx
sub rsp, 32
Live Demo
Конструктор по умолчанию
Как видно из сравнения, компилятор успешно выкинул все вспомогательные runtime вызовы и вычисления сдвигов. Но, тем не менее, относительно производительности есть значимый недостаток — при обходе полей посетителем посредством конструирования дополнительного объекта, также будут конструироваться поля оригинальных типов (field::value) посредством default initialization. Если у объекта тяжёлый конструктор по-умолчанию, то он будет вызван и тем самым повлияет на производительность.Но, к счастью, большинство конструкторов по умолчанию простые, и могут быть выкинуты компилятором — так как эти поля никак не используются — как например произошло в случае со std::vector выше (собственно для демонстрации этого он и был туда добавлен).
Как вариант можно добавить дополнительный шаблонный параметр в field и протаскивать его из внешней структуры, и в зависимости от его состояния глушить создание лишних полей, но это уже намного более громоздко для пользователя — лучше наверное взять BOOST_FUSION_DEFINE_STRUCT/BOOST_HANA_DEFINE_STRUCT, X Macros и прочие макросы высшего порядка.
Альтернативные возможности
Есть и другие варианты с помощью можно "пошевилить" поля не обращаясь к ним по имени, и они доступны даже для C++98. Например для этой же цели можно запрячь конструктор копирования или оператор присваивания сгенерированные компилятором — ведь там автоматически осуществляется поэлементное передёргивание полями. Но, в таком случае код посещения будет в обёртке field, и видимо придётся жонглировать через глобальный/TLS контекст, что уже крайне маловероятно поддастся оптимизатору.Файлы
for_each_field.hpp | |
| |
versus_handwritten.cpp | |
| |
for_each_field.s | |
| |
handwritten.s | |
| |
versus_handwritten.filtered.s | |
| |