【问题标题】:Variadic function template with pack expansion not in last parameter具有包扩展的可变参数函数模板不在最后一个参数中
【发布时间】:2013-01-24 00:07:38
【问题描述】:

我想知道为什么下面的代码不能编译:

struct S
{
    template <typename... T>
    S(T..., int);
};

S c{0, 0};

此代码无法使用 clang 和 GCC 4.8 编译。这是 clang 的错误:

test.cpp:7:3: error: no matching constructor for initialization of 'S'
S c{0, 0};
  ^~~~~~~
test.cpp:4:5: note: candidate constructor not viable: requires 1 argument, but 2 were provided
    S(T..., int);
    ^

在我看来这应该可行,并且 T 应该被推断为长度为 1 的包。

如果标准禁止这样做,有谁知道为什么?

【问题讨论】:

  • 一个原因是S(T..., U...)S(T..., int = 0) 将无法解决。所以规则说你只能有一包,而且必须是最后一包。
  • @BoPersson:我不明白这是禁止S(T..., int) 的理由,因为这既不是默认参数也不是第二个参数包。
  • 它会创建很多特殊的规则。无论如何,参数包已经够难了,所以委员会选择了一个简单而通用的规则。
  • @HighCommander4 (x,y,z,a,b,c,.....infinity ,Last) ,你能推断出Last 如果通过(12,3,4) 的值是多少吗? ,如果你回答 Last = 4,那么在参数列表的开头直到无穷大的 case 参数不可以取那个值吗?

标签: c++ templates c++11 variadic-templates function-templates


【解决方案1】:

因为当函数形参包不是最后一个形参时,不能从中推导出模板形参包,模板实参推导将忽略它。

所以两个参数 0, 0, int 进行比较,结果不匹配。

像这样的推导规则需要涵盖许多特殊情况(例如当两个参数包彼此相邻出现时会发生什么)。由于参数包是 C++11 中的一项新功能,因此相应提案的作者保守地起草了规则。

请注意,如果没有以其他方式推导,尾随模板参数包将为空。所以当你用一个参数调用构造函数时,事情就会起作用(注意这里模板参数包和函数参数包的区别。前者是尾随,后者不是)。

【讨论】:

    【解决方案2】:

    所以,应该有一个解决方法。大致如下:

    namespace v1 {
      // Extract the last type in a parameter pack.
      // 0, the empty pack has no last type (only called if 1 and 2+ don't match)
      template<typename... Ts>
      struct last_type {};
    
      // 2+ in pack, recurse:
      template<typename T0, typename T1, typename... Ts>
      struct last_type<T0, T1, Ts...>:last_type<T1, Ts...>{};
    
      // Length 1, last type is only type:
      template<typename T0>
      struct last_type<T0> {
        typedef T0 type;
      };
    }
    namespace v2 {
      template<class T> struct tag_t{using type=T;};
      template<class T> using type_t = typename T::type;
      template<class...Ts>
      using last = type_t< std::tuple_element_t< sizeof...(Ts)-1, std::tuple<tag_t<Ts>...> > >;
      template<class...Ts>
      struct last_type {
        using type=last<Ts...>;
      };
    }
    template<class...Ts>
    using last_type=v2::late_type<Ts...>; // or v1   
    
    
    struct S
    {
        // We accept any number of arguments
        // So long as the type of the last argument is an int
        // probably needs some std::decay to work right (ie, to implicitly work out that
        // the last argument is an int, and not a const int& or whatever)
        template <typename... T, typename=typename std::enable_if<std::is_same<int, typename last_type<T...>::type>>::type>
        S(T...);
    
    };
    

    我们检查参数包的最后一种类型是int,或者我们只传递了int

    【讨论】:

      【解决方案3】:

      我实际上对同一件事有点兴趣(想根据最终参数专门化模板化参数包)。

      我相信通过结合元组反转(std::make_tuple,后向端口std::apply 用于 C++14 等)可能会有一条前进的道路:

      如果成功了会回到这里。

      相关帖子:

      编辑: 是的,过了一会儿想通了;不完美,因为有额外的副本飞来飞去,但这是一个开始。

      如果您知道比我在下面列出的更简单的方法,请不要犹豫发布!

      TL;DR

      可以做这样的事情:

      auto my_func_callable = [] (auto&& ... args) {
          return my_func(std::forward<decltype(args)>(args)...);
      };
      auto my_func_reversed =
          stdcustom::make_callable_reversed(my_func_callable);
      

      然后实现这个伪代码:

      template<typename ... Args>
      void my_func(Args&& ... args, const my_special_types& x);
      

      通过执行以下操作:

      template<typename... Args>
      void my_func(Args&& ... args)
          -> call my_func_reversed(args...)
      template<typename... RevArgs>
      void my_func_reversed(const my_special_types& x, RevArgs&&... revargs)
          -> do separate things with revargs and my_special_types
          -> sub_func_reversed(revargs...)
      

      使用上述实用程序。

      有一些(很多)缺点。将在下面列出。

      范围

      这是为 C++14(可能是 C++11)的用户准备的,他们想从未来(C++17)中借鉴。

      第 1 步:反转参数

      有几种不同的方法可以做到这一点。我在这个例子中列出了一些替代方案:

      • tuple.cc - 两个替代方案的游乐场(源代码中的学分):
        1. 使用可折叠表达式并操作通过std::apply_impl 传递的索引(来源:Orient)。
        2. 使用递归模板构造一个反向的index_sequence(来源:Xeo)
      • tuple.output.txt - 示例输出

        • 这会打印出 Xeo 示例中的 reversed_index_sequence 模板。我需要这个来调试。

          >>> name_trait<std::make_index_sequence<5>>::name()
          std::index_sequence<0, 1, 2, 3, 4>
          >>> name_trait<make_reversed_index_sequence<5>>::name()
          std::index_sequence<4, 3, 2, 1, 0>
          

      我选择了备选方案 1,因为它更容易消化。 然后我尝试快速将其正式化:

      定义片段(改编自 cppreference.com 上 std::apply 的 C++17 可能实现):

      namespace detail {
      template <class F, class Tuple, std::size_t... I>
      constexpr decltype(auto) apply_reversed_impl(F &&f,
          Tuple &&t, std::index_sequence<I...>) 
      {
          // @ref https://stackoverflow.com/a/31044718/7829525
          // Credit: Orient
          constexpr std::size_t back_index = sizeof...(I) - 1;
          return f(std::get<back_index - I>(std::forward<Tuple>(t))...);
      }
      } // namespace detail
      template <class F, class Tuple>
      constexpr decltype(auto) apply_reversed(F &&f, Tuple &&t) 
      {
          // Pass sequence by value to permit template inference
          // to parse indices as parameter pack
          return detail::apply_reversed_impl(
              std::forward<F>(f), std::forward<Tuple>(t),
              std::make_index_sequence<
                  std::tuple_size<std::decay_t<Tuple>>::value>{});
      }
      

      用法片段:(来自tuple_future_main.output.txt,从上面复制)

      auto my_func_callable = [] (auto&& ... args) {
          return my_func(std::forward<decltype(args)>(args)...);
      };
      auto my_func_reversed =
          stdcustom::make_callable_reversed(my_func_callable);
      

      第 2 步:扣鞋(使用反向参数包)

      首先,为您希望使用的最终参数建立模式。您必须明确列举这些,因为您只能有一个参数包。

      (取自tuple_future_main.cc):

      示例场景:

      我们喜欢将东西添加到具有名称的容器中,例如:

      add_item(const Item& item, const string& name, Container& c)
      

      我们还可以构造一个具有 [非常大] 数量的重载的 Item,并且 我们有方便的重载:

      add_item(${ITEM_CTOR_ARGS}, const string& name, Container& c)
      

      为此,我们可以声明以下内容:

      void add_item_direct(const Item& item, const string& name, Container& c)
      Item create_item(Args&&... args)
      

      然后定义我们的通用接口:

      template<typename... Args>
      void add_item(Args&&... args) {
          ...
          auto reversed = stdcustom::make_callable_reversed(callable);
          reversed(std::forward<Args>(args)...);
      }
      template<typename ... RevArgs>
      void add_item_reversed(Container& c, const string& name, RevArgs&&... revargs)
      {
          ...
          static auto ctor = VARIADIC_CALLABLE(create_item,);
          ...
          auto item = ctor_reversed(std::forward<RevArgs>(revargs)...);
          add_item_direct(item, name, c);
      }
      

      现在我们可以执行以下操作:(取自tuple_future_main.output.txt

      >>> (add_item(Item("attribute", 12), "bob", c));
      >>> (add_item("attribute", 12, "bob", c));
      >>> (add_item(Item(2, 2.5, "twelve"), "george", c));
      >>> (add_item(2, 2.5, "twelve", "george", c));
      >>> (add_item(Item(2, 15.), "again", c));
      >>> (add_item(2, 15., "again", c));
      >>> c
      bob - ctor3: ctor3: ctor1: attribute (12, 10)
      bob - ctor3: ctor1: attribute (12, 10)
      george - ctor3: ctor3: ctor2: 2, 2.5 (twelve)
      george - ctor3: ctor2: 2, 2.5 (twelve)
      again - ctor3: ctor3: ctor2: 2, 15 ()
      again - ctor3: ctor2: 2, 15 ()
      

      注意额外的复制构造函数... :(

      缺点

      • 丑得要命
      • 可能没用
        • 重构你的接口可能更容易
          • 但是,这可以用作过渡到更通用界面的权宜之计。
          • 可能要删除的行数更少。
        • 尤其是当它在您的开发过程中插入模板爆炸时
      • 无法确定额外副本的来源。
        • 这可能是由于对可变参数 lambda 的明智使用
      • 您必须精心设计基础功能
        • 您不应尝试扩展现有函数。
        • 参数包会贪婪地匹配函数
        • 您要么需要明确说明您想要的每个重载,要么弯下腰让可变参数包分派到您想要的功能
          • 如果您找到解决此问题的优雅方法,请告诉我。
      • 模板错误很糟糕。
        • 当然,不是太糟糕。但很难推断您错过了可用的过载。
      • 在 lambda 中封装了许多简单的功能
        • 您也许可以使用make_reversed_index_sequence 并直接调度到函数(在其他 SO 帖子中提到)。但这重复起来很痛苦。

      待办事项

      • 摆脱多余的副本
      • 尽量减少对所有 lambda 的需求
        • 如果您有Callable,则不需要
      • 尝试对抗参数包贪婪

        • 是否有一个通用的std::enable_if 匹配同时匹配左值和右值引用,并且可能处理转发兼容的隐式复制构造函数?

          template<typename ... Args>
          void my_func(Args&& ... args) // Greedy
          void my_func(magical_ref_match<string>::type, ...)
              // If this could somehow automatically snatch `const T&` and `T&&` from the parameter pack...
              // And if it can be used flexible with multiple arguments, combinatorically
          

      希望

      • 也许 C++17 将支持非最终参数包参数,这样所有这些都可以被丢弃...... 手指交叉

      【讨论】:

      • (个人帐户,稍后合并)刚刚看到另一个可能更好的扩展名:stackoverflow.com/a/15904742/170413 看看 Andy 通过 revert_call 进行完美转发的机制是否比这个更好。
      • 总的来说,为了我想做的事情,决定放弃这种方法,而只追求重构。然而,我确实有点研究如何对抗贪婪匹配,利用其他一些窥视者的帖子:stackoverflow.com/a/43510740/170413
      【解决方案4】:

      从标准 N3376 § 14.1 的工作草案中可以阅读有关此内容的部分。

      以下是第 14.1.11 节

      如果类模板或别名模板的模板参数具有 默认模板参数,每个后续模板参数应 要么提供一个默认的模板参数,要么是一个模板 参数包。如果主类模板的模板参数或 别名模板是模板参数包,应该是最后一个 模板参数。函数模板的模板参数包 后面不应跟另一个模板参数,除非 模板参数可以从参数类型列表中推导出来 函数模板或具有默认参数。

      【讨论】:

      • -1,因为我看不出这个标准的引用有什么帮助。
      • @JohannesSchaub-litb:只是因为 Rapptz 没有粗体相关句子,而你没有发现它。
      • @LightnessRacesinOrbit 没有相关的句子,不是吗?您加粗的内容涉及类模板和别名模板。我还没有在问题中看到一个。
      • @JohannesSchaub-litb:是的,好的。这是一个更有意义的否决解释,那么:这里唯一适用的段落是讨论演绎的最后一段,这已经是问题的一部分。
      • @LightnessRacesinOrbit 我不明白最后一段是如何应用的。模板参数包后面没有问题代码中的其他模板参数。
      猜你喜欢
      • 2015-05-21
      • 2014-10-30
      • 1970-01-01
      • 1970-01-01
      • 2011-10-19
      • 1970-01-01
      相关资源
      最近更新 更多