【问题标题】:C++ parameter pack, constrained to have instances of a single type?C ++参数包,仅限于具有单一类型的实例?
【发布时间】: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) 没有意义。

标签: c++ templates c++11 variadic-templates visitor-pattern


【解决方案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 禁用重载
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多