С С++ 11 мы можем создавать функции шаблона, которые могут принимать любую последовательность аргументов:
template <typename... Ts>
void func(Ts &&... ts) {
step_one(std::forward<Ts>(ts)...);
step_two(std::forward<Ts>(ts)...);
}
Однако предположим, что действительно имеет смысл вызвать мою функцию в случае, когда каждый аргумент имеет один и тот же тип - любое количество аргументов будет в порядке.
Какой лучший способ сделать это, т.е. есть хороший способ ограничить шаблоны, чтобы сделать приятное сообщение об ошибке в этом случае или, в идеале, исключить func
от участия в разрешении перегрузки, когда аргументы не совпадают
Я могу сделать это действительно конкретным, если это поможет:
Предположим, что у меня есть некоторая структура:
struct my_struct {
int foo;
double bar;
std::string baz;
};
Теперь я хочу иметь возможность делать такие вещи, как печатать члены структуры для целей отладки, сериализовать и десериализовать структуру, посещать членов структуры последовательно и т.д. У меня есть код, который поможет с этим
template <typename V>
void apply_visitor(V && v, my_struct & s) {
std::forward<V>(v)("foo", s.foo);
std::forward<V>(v)("bar", s.bar);
std::forward<V>(v)("baz", s.baz);
}
template <typename V>
void apply_visitor(V && v, const my_struct & s) {
std::forward<V>(v)("foo", s.foo);
std::forward<V>(v)("bar", s.bar);
std::forward<V>(v)("baz", s.baz);
}
template <typename V>
void apply_visitor(V && v, my_struct && s) {
std::forward<V>(v)("foo", std::move(s).foo);
std::forward<V>(v)("bar", std::move(s).bar);
std::forward<V>(v)("baz", std::move(s).baz);
}
(Это выглядит немного сложно, чтобы генерировать код, подобный этому, но я сделал небольшую библиотеку некоторое время назад, чтобы помочь с этим.)
Итак, теперь я хотел бы расширить его, чтобы он мог одновременно посещать два экземпляра my_struct
. Использование этого в том, что если я хочу реализовать операции равенства или сравнения. В документации boost::variant
они называют это "двоичное посещение" в отличие от "унарных посещений".
Вероятно, никто не захочет делать больше, чем бинарное посещение. Но предположим, что я хочу сделать, вообще, n-ary
посещение. Тогда, похоже, я думаю,
template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
std::forward<V>(v)("foo", (std::forward<Ss>(ss).foo)...);
std::forward<V>(v)("bar", (std::forward<Ss>(ss).bar)...);
std::forward<V>(v)("baz", (std::forward<Ss>(ss).baz)...);
}
Но теперь это становится немного более безвозвратно - если кто-то передает серию типов, которые даже не имеют одинакового типа структуры, код все еще может компилироваться и делать что-то совершенно неожиданное для пользователя.
Я думал о том, как это сделать:
template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
auto foo_ptr = &my_struct::foo;
std::forward<V>(v)("foo", (std::forward<Ss>(ss).*foo_ptr)...);
auto bar_ptr = &my_struct::bar;
std::forward<V>(v)("bar", (std::forward<Ss>(ss).*bar_ptr)...);
auto baz_ptr = &my_struct::baz;
std::forward<V>(v)("baz", (std::forward<Ss>(ss).*baz_ptr)...);
}
Это, по крайней мере, вызовет ошибку компиляции, если они будут использовать ее с несоответствующими типами. Но это также происходит слишком поздно - это происходит после того, как типы шаблонов решены, и после разрешения перегрузки я предполагаю.
Я думал об использовании SFINAE, например, вместо возврата void, используя std::enable_if_t
и проверяя некоторое выражение std::is_same<std::remove_cv_t<std::remove_reference_t<...>>
для каждого типа в пакете параметров.
Но для одного это выражение SFINAE довольно сложно, а для двоих оно также имеет недостаток - предположим, что у кого-то есть производный класс struct my_other_struct : my_struct { ... }
, и они хотят использовать его с механизмом посетителя, поэтому некоторые из параметры my_struct
, а некоторые - my_other_struct
. В идеале система конвертирует все ссылки на my_struct
и применяет посетителя таким образом, а афайк, который я привел выше, с указателями элементов foo_ptr
, bar_ptr
, baz_ptr
сделал бы там правильные вещи, но это даже не ясно, как написать ограничение, подобное этому с SFINAE - мне нужно будет попытаться найти общую базу всех параметров, которые я предполагаю?
Есть ли хороший способ примирить эти проблемы в целом?