20

我想知道为什么以下代码无法编译:

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 的包。

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

4

4 回答 4

10

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

因此,将这两个参数0, 0与 进行比较, int,产生不匹配。

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

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

于 2013-02-09T12:22:31.437 回答
6

因此,应该有一个解决方法。这些方面的东西:

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

于 2013-02-08T21:40:41.260 回答
3

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

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

如果成功的话会回到这里。

相关文章:

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

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

TL;博士

可以做这样的事情:

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(credit: Orient) 传递的索引。
    2. 使用递归模板构造一个反向的index_sequence(来源:Xeo)
  • tuple.output.txt - 示例输出

    • 这将打印出reversed_index_sequenceXeo 示例中的模板。我需要这个来调试。

      >>> 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,因为它更容易消化。然后,我尝试快速将其正式化:

std::apply定义片段(改编自cppreference.com 上的 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 的明智使用
  • 你必须精心设计你的基本功能
    • 您不应该尝试扩展现有功能。
    • 参数包会贪婪地匹配函数
    • 您要么需要明确说明您想要的每个重载,要么屈服并让可变参数包调度到您想要的功能
      • 如果您找到解决此问题的优雅方法,请告诉我。
  • 模板错误很糟糕。
    • 诚然,不算太坑爹。但是很难推断您错过了可用的过载。
  • 在 lambdas 中包装了许多简单的功能
    • 您可能能够使用make_reversed_index_sequence并直接分派给该功能(在其他 SO 帖子中提到)。但这重复起来很痛苦。

去做

  • 摆脱多余的副本
  • 最小化对所有 lambdas 的需求
    • 如果您有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 将支持非最终参数包参数,这样所有这些都可以被丢弃......手指交叉
于 2017-04-19T01:52:08.450 回答
2

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

以下是第 14.1.11 节

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

于 2013-02-08T09:09:45.663 回答