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

Posted

技术标签:

【中文标题】C ++参数包,仅限于具有单一类型的实例?【英文标题】:C++ parameter pack, constrained to have instances of a single type? 【发布时间】:2016-11-26 11:54:53 【问题描述】:

从 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 文档中,他们称其为“二元访问”,与“一元访问”形成对比。

可能没有人愿意做比二进制访问更多的事情。但假设我想做喜欢,一般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&lt;std::remove_cv_t&lt;std::remove_reference_t&lt;...&gt;&gt;

但是一方面,SFINAE 表达式相当复杂,另一方面,它也有一个缺点——假设有人有一个派生类struct my_other_struct : my_struct ... ,并且他们想将它与访问者机制一起使用,所以一些参数是my_struct,有些是my_other_struct。理想情况下,系统会将所有引用转换为my_struct 并以这种方式应用访问者,并且我上面给出的带有成员指针foo_ptrbar_ptrbaz_ptr 的示例会在那里做正确的事情,但它是我什至不清楚如何用 SFINAE 编写这样的约束——我必须尝试找到我猜想的所有参数的共同基础?

有没有一种好方法可以调和这些顾虑?

【问题讨论】:

std::common_type? 您希望它们都属于特定的已知类型,还是只是相同的未知类型? @cpplearner:哦,那很好,我不知道这个 @Quentin:我想如果它会有所作为,我想看看一个例子。我想知道一种做更一般的方法的好方法——相同的、未知的类型 多次forward&lt;V&gt;(v) 没有意义。 【参考方案1】:

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

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


这只会保证C++17 开始对SFINAE 友好。 ClangGCC 都已经以这种方式实现了。

【讨论】:

@JohanLundberg 我把它放在那里是为了模拟参数传递,但经过一些测试,它实际上破坏了传递引用。固定。 common_type 确定它是否存在一个通用类型,所有其他类型都可以隐式转换为该类型。这并不意味着它们都是同一类型。 @skypjack OP 中的最后一段表明这是所需的行为。【参考方案2】:

这是一个类型特征,您可以在闲暇时在 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&lt;Derived, Base&gt;::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; 无法编译)。

【讨论】:

【参考方案3】:

你真正想要的是这样的:

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

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

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

以上没有真正的目的:IdxType&lt;T, n&gt; 只是任何nT。但是,它允许您这样做:

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

这很好,因为这正是您获得一组相同类型的可变参数所需要的。剩下的唯一问题是你不能做像myFunc(obj1, obj2, obj3) 这样的事情,因为编译器将无法推断出所需的Indices - 你将不得不做myFunc&lt;1,2,3&gt;(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;

【讨论】:

这是一个不错的方法,我什至没有想过这样做【参考方案4】:

假设只有在每个参数具有相同类型的情况下调用我的函数才有意义——不过,任何数量的参数都可以。

在这种情况下,为什么不使用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

【讨论】:

仅限于const 的数据副本。【参考方案5】:

这采用任意 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&lt;my_struct&gt; 无法对Ss 进行评估,则匹配失败。

live example

【讨论】:

我不认为你可以构造一个void_t,因为它只是void的别名 @GuyGreer void() 有效。不确定void 是否是,所以我删除了它。现在有一个活生生的例子,所以至少有 2 个编译器喜欢它。【参考方案6】:

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

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

【讨论】:

【参考方案7】:

我认为您可以创建这样的函数并检查函数内部的参数。

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;

【讨论】:

这将不允许签入 static_assert 或使用 enable_if 禁用重载

以上是关于C ++参数包,仅限于具有单一类型的实例?的主要内容,如果未能解决你的问题,请参考以下文章

C ++继承:具有基类类型的虚函数中派生类类型的参数

仅在生产中出错-“指定的值不是'Edm.Int32'类型的实例参数名称:值”

c ++继承-具有不同参数类型的相同方法名称

如何使用模板参数包实现 SFINAE 仅限于少数类型

具有约束参数功能的类型类

在 Cython 中包装具有非标准类型作为参数的 C 函数