Ответ 1
С std::common_type
это просто:
template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {
}
Это будет гарантировано только SFINAE
-дружественно от C++17
. Clang
и GCC
уже реализованы так.
С С++ 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 - мне нужно будет попытаться найти общую базу всех параметров, которые я предполагаю?
Есть ли хороший способ примирить эти проблемы в целом?
С std::common_type
это просто:
template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {
}
Это будет гарантировано только SFINAE
-дружественно от C++17
. Clang
и GCC
уже реализованы так.
Вот черта типа, которую вы можете использовать в static_assert
или std::enable_if
на досуге.
template <class T, class ... Ts>
struct are_all_same : conjunction<std::is_same<T, Ts>...>{};
template <class Ts...>
struct conjunction : std::true_type{};
template <class T, class ... Ts>
struct conjunction<T, Ts...> :
std::conditional<T::value, conjunction<Ts...>, std::false_type>::type {};
Он просто проверяет каждый тип с первым и терпит неудачу, если он отличается.
Я думаю, что использование std::common_type
будет выглядеть примерно так:
template <class ... Args>
typename std::common_type<Args...>::type common_type_check(Args...);
void common_type_check(...);
template <class ... Ts>
struct has_common_type :
std::integral_constant<
bool,
!std::is_same<decltype(common_type_check(std::declval<Ts>()...)), void>::value> {};
Затем вы можете сделать static_assert(std::has_common_type<Derived, Base>::value, "")
Конечно, этот метод не является надежным, поскольку common_type
имеет некоторые ограничения, когда дело доходит до базовых классов:
struct A {};
struct B : A{};
struct C : A{};
struct D : C{};
struct E : B{};
static_assert(has_common_type<E, D, C, A, B>::value, ""); //Fails
static_assert(has_common_type<A, B, C, D, E>::value, ""); //Passes
Это связано с тем, что шаблон сначала пытается получить общий тип между D
и E
(т.е. auto a = bool() ? D{}: E{};
не удается скомпилировать).
То, что вы действительно хотите, это что-то вроде:
template<typename T, T ... args>
void myFunc(T ... args);
Но, очевидно, это не юридический синтаксис. Однако вы можете обойти эту проблему с помощью шаблона using
, который поможет вам. Итак, идея такова:
template<typename T, size_t val>
using IdxType = T;
Вышеизложенное не имеет реальной цели: a IdxType<T, n>
- это всего лишь T
для любого n
. Однако он позволяет это сделать:
template<typename T, size_t ... Indices>
void myFunc(IdxType<T, Indices> ... args);
Это здорово, так как это именно то, что вам нужно, чтобы получить вариационный набор идентично типизированных параметров. Единственная проблема, которая остается в том, что вы не можете делать такие вещи, как myFunc(obj1, obj2, obj3)
, поскольку компилятор не сможет вывести требуемый Indices
- вам нужно будет сделать myFunc<1,2,3>(obj1, obj2, obj3)
, что является уродливым. К счастью, вы можете уйти от этого, обернув в вспомогательную функцию, которая заботится о генерации индекса для вас, используя make_index_sequence
.
Ниже приведен полный пример, который похож на вашего посетителя (live demo здесь):
template<typename T, size_t sz>
using IdxType = T;
struct MyType
{};
struct Visitor
{
void operator() (const MyType&)
{
std::cout << "Visited" << std::endl;
}
};
template <typename V>
void apply_visitor(std::index_sequence<>, V && v)
{
}
template <typename V, typename T, size_t FirstIndex, size_t ... Indices>
void apply_visitor(std::index_sequence<FirstIndex, Indices...>, V && v, T && first, IdxType<T, Indices> && ... ss) {
std::forward<V>(v)(std::forward<T>(first));
apply_visitor(std::index_sequence<Indices...>(), std::forward<V>(v), std::forward<T>(ss) ...);
}
template <typename V, typename T, typename ... Rest>
void do_apply_visitor(V && v, T && t, Rest && ... rest )
{
apply_visitor(std::make_index_sequence<sizeof...(Rest)+1>(), v, t, rest ... );
}
int main()
{
Visitor v;
do_apply_visitor(v, MyType{}, MyType{}, MyType{});
return 0;
}
Это принимает произвольный тип In
и перемещает его ссылку r/lvalue на тип Out
в неявном литье.
template<class Out>
struct forward_as {
template<class In,
std::enable_if_t<std::is_convertible<In&&,Out>{}&&!std::is_base_of<Out,In>{},int>* =nullptr
>
Out operator()(In&& in)const{ return std::forward<In>(in); }
Out&& operator()(Out&& in)const{ return std::forward<Out>(in); }
template<class In,
std::enable_if_t<std::is_convertible<In&,Out&>{},int>* =nullptr
>
Out& operator()(In& in)const{ return in; }
template<class In,
std::enable_if_t<std::is_convertible<In const&,Out const&>{},int>* =nullptr
>
Out const& operator()(In const& in)const{ return in; }
};
При этом вот наш n-ary apply_visitor
:
template <typename V, typename ... Ss,
decltype(std::void_t<
std::result_of_t<forward_as<my_struct>(Ss)>...
>(),int())* =nullptr
>
void apply_visitor(V && v, Ss && ... ss) {
auto convert = forward_as<my_struct>{};
std::forward<V>(v)("foo", (convert(std::forward<Ss>(ss)).foo)...);
std::forward<V>(v)("bar", (convert(std::forward<Ss>(ss)).bar)...);
std::forward<V>(v)("baz", (convert(std::forward<Ss>(ss)).baz)...);
}
который не соответствует, если forward_as<my_struct>
не может оцениваться на Ss
.
предположим, что действительно имеет смысл вызывать мою функцию в случае, когда каждый аргумент имеет один и тот же тип - любое количество аргументов будет в порядке.
В этом случае, почему бы не использовать std::initializer_list
?
template <typename T>
void func(std::initializer_list<T> li) {
for (auto ele : li) {
// process ele
cout << ele << endl;
}
}
Как упоминалось в комментарии @Yakk, вы можете избежать копирования const
. В этом случае вы можете скопировать указатели на std::initializer_list
:
// Only accept pointer type
template <typename T>
void func(std::initializer_list<T> li) {
for (auto ele : li) {
// process pointers, so dereference first
cout << *ele << endl;
}
}
Или специализируйте func
для указателей:
// Specialize for pointer
template <typename T>
void func(std::initializer_list<T*> li) {
for (auto ele : li) {
// process pointers, so dereference first
cout << *ele << endl;
}
}
my_struct a, b, c;
func({a, b, c}); // copies
func({&a, &b, &c}); // no copies, and you can change a, b, c in func
В следующем примере можно использовать функцию времени компиляции, такую как are_same
:
#include <type_traits>
template<typename T, typename... O>
constexpr bool are_same() {
bool b = true;
int arr[] = { (b = b && std::is_same<T, O>::value, 0)... };
return b;
}
int main() {
static_assert(are_same<int, int, int>(), "!");
static_assert(not are_same<int, double, int>(), "!");
}
Используйте его, как следует:
template <typename... Ts>
void func(Ts &&... ts) {
static_assert(are_same<Ts...>(), "!");
step_one(std::forward<Ts>(ts)...);
step_two(std::forward<Ts>(ts)...);
}
У вас будет хорошее сообщение об ошибке времени компиляции в соответствии с запросом.
Я думаю, вы можете сделать такую функцию и проверить аргументы внутри своей функции.
template <typename T, typename... Args> bool check_args(T a, Args args)
{
static string type;
if(type == "") type = typeid(a).name;
else if(type != typeid(a).name) return false;
else return check_args(args...);
}
bool check_args() {return true;}