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
слайды
В этом примере обходятся поля свежеопределенной структуры
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
Нам требуется каким-то образом опросить все поля, причём не называя их по имени. Трюк заключается в вытаскивании данных о полях через список инициализации агрегата.
Например объект типа
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.
Это всё конечно на грани
фола UB, и нужно внимательно свериться со стандартом, выбрать правильные ограничения (standard layout type?). Но даже в случае если в каком-то из вариантов окажется UB — то на конкретных компиляторах это вполне может работать, необходимо лишь скрупулёзно протестировать.
Теперь, для того чтобы обойти все поля, нам нужно узнать их количество — это осуществляется через 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 не нужен, так как результирующий ассемблерный код в обоих вариантах идентичен
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
--- 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
Как видно из сравнения, компилятор успешно выкинул все вспомогательные 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 |
| // Copyright Evgeny Panasyuk 2017.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
// e-mail: E?????[dot]P???????[at]gmail.???
// for_each_field Proof-of-Concept
#ifndef FOR_EACH_FIELD_HPP
#define FOR_EACH_FIELD_HPP
/******************************************************************************/
#include <type_traits>
#include <utility>
#include <tuple>
#include <array>
/******************************************************************************/
namespace proof_of_concept
{
/******************************************************************************/
#ifdef __GNUC__
#define NOINLINE __attribute__((noinline))
#else
#define NOINLINE
#endif
/******************************************************************************/
/****************************************************************/
struct to_any
{
template<typename T> operator T();
};
/****************************************************************/
template<typename T, typename Pack> struct arity_test;
template<typename T, typename ...Xs>
struct arity_test<T, std::tuple<Xs...>>
{
using yes_type = std::array<char, 2>;
using no_type = std::array<char, 1>;
template<typename U>
static auto check(int) ->
decltype(U{Xs{}...}, yes_type{});
template<typename U>
static no_type check(...);
constexpr static auto value =
sizeof(check<T>(0)) == sizeof(yes_type);
};
/****************************************************************/
template
<
typename T,
typename Pack = std::tuple<to_any>,
bool cond = arity_test<T, Pack>::value
>
struct fields_count_aux
{
constexpr static auto value = fields_count_aux
<
T,
decltype(std::tuple_cat(Pack{}, std::tuple<to_any>{}))
>::value;
};
template<typename T, typename Pack>
struct fields_count_aux<T, Pack, false>
{
constexpr static auto value = std::tuple_size<Pack>::value - 1;
// or std::tuple_size_v when available
};
template<typename T>
constexpr auto fields_count =
fields_count_aux<std::remove_reference_t<T>>::value;
/****************************************************************/
template<typename F>
struct field_visitor
{
F f;
template<typename T>
void operator()(T *field_this)
{
f(field_this);
}
};
template<>
struct field_visitor<void> // for is_fields_sequence
{
template<typename T> void operator()(T*);
};
template<typename F>
constexpr auto make_field_visitor(F f)
{
return field_visitor<F>{f};
}
template<typename T>
struct field
{
template<typename U>
field(field_visitor<U> &f)
{
f(this);
}
field(field_visitor<void>); // for is_fields_sequence
// std::forward would require constructor template
// and additional restriction
field(T x) : value(std::move(x)) {}
field(T &x) : value(x) {}
field() {}
T value;
};
/****************************************************************/
template<typename T, typename F, size_t ...Is>
void for_each_field_aux(T &x, F &f, std::index_sequence<Is...>)
{
std::aligned_union_t<1, T> storage;
auto placement_addr = reinterpret_cast<char*>(&storage);
auto action = make_field_visitor([&](auto *field_this)
{
using field_type = std::remove_reference_t<decltype(*field_this)>;
auto byte_shift = reinterpret_cast<char*>(field_this) - placement_addr;
auto field_addr = reinterpret_cast<char*>(&x) + byte_shift;
auto current_field = reinterpret_cast<field_type*>(field_addr);
f(current_field->value);
});
(void)action;
new (placement_addr) T{((void)Is, action)...};
reinterpret_cast<T*>(placement_addr)->~T();
}
template<typename T, typename F>
auto for_each_field(T &&x, F f)
{
for_each_field_aux(x, f, std::make_index_sequence<fields_count<T>>{});
return f;
}
/****************************************************************/
template<typename T, size_t ...Is>
constexpr bool is_fields_sequence_aux(std::index_sequence<Is...>)
{
return arity_test
<
T,
std::tuple<decltype(((void)Is, field_visitor<void>{}))...>
>::value;
}
template<typename T>
constexpr bool is_fields_sequence()
{
return is_fields_sequence_aux<T>
(
std::make_index_sequence<fields_count<T>>{}
);
}
/****************************************************************/
} // namespace end
#endif
|
| |
| versus_handwritten.cpp |
| #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);
});
}
|
| |
| for_each_field.s |
| test_for_each_field(Widget&):
.LFB1457:
push rbx
.seh_pushreg rbx
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
mov rbx, rcx
call void use_field<int>(int&)
lea rcx, 8[rbx]
call void use_field<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)
lea rcx, 32[rbx]
add rsp, 32
pop rbx
jmp void use_field<char>(char&)
.seh_endproc
|
| |
| handwritten.s |
| test_handwritten(Widget&):
.LFB1456:
push rbx
.seh_pushreg rbx
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
mov rbx, rcx
call void use_field<int>(int&)
lea rcx, 8[rbx]
call void use_field<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)
lea rcx, 32[rbx]
add rsp, 32
pop rbx
jmp void use_field<char>(char&)
.seh_endproc
|
| |
| versus_handwritten.filtered.s |
| .file "versus_handwritten.cpp"
.intel_syntax noprefix
.text
.p2align 4,,15
.globl test_handwritten(Widget&)
.def test_handwritten(Widget&); .scl 2; .type 32; .endef
.seh_proc test_handwritten(Widget&)
test_handwritten(Widget&):
.LFB1456:
push rbx
.seh_pushreg rbx
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
mov rbx, rcx
call void use_field<int>(int&)
lea rcx, 8[rbx]
call void use_field<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)
lea rcx, 32[rbx]
add rsp, 32
pop rbx
jmp void use_field<char>(char&)
.seh_endproc
.p2align 4,,15
.globl test_for_each_field(Widget&)
.def test_for_each_field(Widget&); .scl 2; .type 32; .endef
.seh_proc test_for_each_field(Widget&)
test_for_each_field(Widget&):
.LFB1457:
push rbx
.seh_pushreg rbx
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
mov rbx, rcx
call void use_field<int>(int&)
lea rcx, 8[rbx]
call void use_field<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&)
lea rcx, 32[rbx]
add rsp, 32
pop rbx
jmp void use_field<char>(char&)
.seh_endproc
.ident "GCC: (GNU) 5.4.0"
.def void use_field<int>(int&); .scl 2; .type 32; .endef
.def void use_field<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >&); .scl 2; .type 32; .endef
.def void use_field<char>(char&); .scl 2; .type 32; .endef
|
| |