C++ 参数包,仅限于具有单一类型的实例?

C++ parameter pack, constrained to have instances of a single type?

从 C++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);
}

(生成这样的代码看起来有点费力,但我前段时间做了 a small library 来帮助解决这个问题。)

所以,现在我想扩展它,以便它可以同时访问 my_struct 的两个实例。那的用途就是,如果我想实现相等或比较操作怎么办。在 boost::variant 文档中,他们称 "binary visitation" 与 "unary visitation" 相对照。

可能没有人会想做比二进制访问更多的事情。但是假设我想做一般 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 表达式非常复杂,另一方面,它也有一个缺点——假设某人有一个派生的 class struct my_other_struct : my_struct { ... },并且他们想将它与访问者机制,所以有些参数是my_struct,有些是my_other_struct。理想情况下,系统会将所有引用转换为 my_struct 并以这种方式应用访问者,并且 afaik 我上面给出的带有成员指针的示例 foo_ptrbar_ptrbaz_ptr 会在那里做正确的事,但我什至不清楚如何用 SFINAE 编写这样的约束——我将不得不尝试找到我猜的所有参数的共同基础?

有什么好的方法可以总体上解决这些问题?

这是一个类型特征,您可以在闲暇时在 static_assertstd::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

这是因为模板首先尝试获取DE之间的公共类型(即auto a = bool() ? D{}: E{};编译失败)。

使用std::common_type,这很简单:

template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {

}

不过,从 C++17 开始,保证 SFINAE 友好。 ClangGCC 都已经这样实现了。

suppose that it really only makes sense to call my function in the case where each argument has the same type -- any number of arguments would be okay though.

既然如此,为什么不用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

这采用任意 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元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 上求值,则无法匹配。

live example

你真正想要的是这样的:

template<typename T, T ... args>
void myFunc(T ... args);

但显然以上不是合法语法。但是,您可以使用模板 using 来帮助您解决这个问题。所以想法是这样的:

template<typename T, size_t val>
using IdxType = T;

以上没有实际目的: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 为您生成索引。

下面是一个完整的例子,与您的访问者相似(现场演示 here):

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;
}

我想你可以像这样创建一个函数并检查函数内部的参数。

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;}

一个可能的解决方案是在以下示例中使用像 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)...);
}

您将收到一条很好的编译时错误消息。