代码之家  ›  专栏  ›  技术社区  ›  user3188445

为什么C++函数参数包必须是占位符或包扩展?

  •  0
  • user3188445  · 技术社区  · 4 年前

    C++20函数参数包的声明符必须是 placeholder pack expansion 。例如:

    // OK, template parameter pack only, no function parameter pack
    template<unsigned ...I> void good1() {}
    
    // OK function parameter pack is pack expansion of decltype(I)
    template<unsigned ...I> void good2(decltype(I)...i) {}
    
    // OK contains placeholder auto
    void good3(std::same_as<unsigned> auto...i) {}
    
    // OK contains placeholder auto
    void good4(std::convertible_to<unsigned> auto...i) {}
    
    // Error, no pack expansion or placeholder
    template<unsigned = 0> void bad(unsigned...i) {}
    

    这似乎使得不可能声明一个接受特定类型的可变数量参数的函数。当然 good2 上面会做,但您必须指定一些伪模板参数,如 good2<0,0,0>(1,2,3) good3 有点像,除非你打电话 good3(1,2,3) 它会失败,你必须写 good3(1U,2U,3U) .我想要一个当你说 good(1, 2U, '\003') --基本上就好像你有无限多的重载函数 good() , good(unsigned) , good(unsigned, unsigned)

    good4 会起作用,只是现在参数实际上不是类型 unsigned ,这可能是一个问题,具体取决于上下文。具体来说,这可能会导致额外的 std::string 复制如下函数:

    void do_strings(std::convertible_to<std::string_view> auto...s) {}
    

    我的问题是:

    1. 我是不是错过了一些技巧,可以让一个人写一个函数,接受特定类型的可变数量的参数?(我想一个例外是C字符串,因为您可以将长度作为参数包,如 template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/} ,但我想对std::size_t这样的类型执行此操作)

    2. 为什么标准会施加这种限制?

    使现代化

    ·询问为什么不使用 好4 结合转发引用以避免额外的副本。我同意 好4 是最接近我想要做的,但参数是不同类型的,有些地方引用也不起作用,这让我很恼火。例如,假设您编写的代码如下:

    void
    good4(std::convertible_to<unsigned> auto&&...i)
    {
      for (auto n : {i...})
        std::cout << n << " ";
      std::cout << std::endl;
    }
    

    你用测试 good(1, 2, 3) 它似乎起了作用。然后有人用你的代码写 good(1, 2, sizeof(X)) 并且它失败,并显示令人困惑的编译器错误消息。当然,答案是写 for (auto n : {unsigned(i)...}) ,在这种情况下,这是可以的,但在其他情况下,您可能会多次使用该包,并且转换运算符非常简单,您只想调用它一次。

    如果您的类型有一个不接触的constexpr转换函数,就会出现另一个恼人的问题 this ,因为在这种情况下,函数将无法在转发引用上工作。诚然,这是高度人为的,但想象一下下面打印“11”的程序:

    template<std::size_t N> std::integral_constant<std::size_t, N> cnst = {};
    
    constexpr std::tuple tpl ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
    
    inline const char *
    stringify(std::convertible_to<decltype(cnst<1>)> auto...i)
    {
      static constexpr const char str[] = { get<i>(tpl)..., '\0' };
      return str;
    }
    
    int
    main()
    {
      std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
    }
    

    如果将参数更改为 stringify 到转发引用 stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i) ,它将无法编译,因为 this

    更新2

    下面是一个更全面的例子来说明为什么 好4 如果你想避免额外的移动/副本,还不够好:

    #include <concepts>
    #include <iostream>
    #include <initializer_list>
    #include <concepts>
    
    struct Tracer {
      Tracer() { std::cout << "default constructed" << std::endl; }
      Tracer(int) { std::cout << "int constructed" << std::endl; }
      Tracer(const Tracer &) { std::cout << "copy constructed" << std::endl; }
      Tracer(Tracer &&) { std::cout << "move constructed" << std::endl; }
      void do_something() const {}
    };
    
    void
    f1(Tracer t1, Tracer t2, Tracer t3)
    {
      t1.do_something();
      t2.do_something();
      t3.do_something();
    }
    
    void
    f2(std::convertible_to<Tracer> auto ...ts)
    {
      (Tracer{ts}.do_something(), ...); // binary fold over comma
    }
    
    void
    f3(std::convertible_to<Tracer> auto&& ...ts)
    {
      (Tracer{std::forward<decltype(ts)>(ts)}.do_something(), ...);
    }
    
    void
    f4(std::initializer_list<Tracer> tl)
    {
      for (const auto &t : tl)
        t.do_something();
    }
    
    void
    f5(std::convertible_to<Tracer> auto&& ...ts)
    {
      std::initializer_list<Tracer> tl { std::forward<decltype(ts)>(ts)... };
      for (const auto &t : tl)
        t.do_something();
    }
    
    int
    main()
    {
      Tracer t;
      std::cout << "=== f1(t, 0, {}) ===" << std::endl;
      f1(t, 0, {});
      std::cout << "=== f2(t, 0, Tracer{}) ===" << std::endl;
      f2(t, 0, Tracer{});
      std::cout << "=== f3(t, 0, Tracer{}) ===" << std::endl;
      f3(t, 0, Tracer{});
      std::cout << "=== f4({t, 0, {}}) ===" << std::endl;
      f4({t, 0, {}});
      std::cout << "=== f5(t, 0, Tracer{}) ===" << std::endl;
      f5(t, 0, Tracer{});
      std::cout << "=== done ===" << std::endl;
    }
    

    程序的输出为:

    default constructed
    === f1(t, 0, {}) ===
    default constructed
    int constructed
    copy constructed
    === f2(t, 0, Tracer{}) ===
    default constructed
    copy constructed
    copy constructed
    int constructed
    copy constructed
    === f3(t, 0, Tracer{}) ===
    default constructed
    copy constructed
    int constructed
    move constructed
    === f4({t, 0, {}}) ===
    copy constructed
    int constructed
    default constructed
    === f5(t, 0, Tracer{}) ===
    default constructed
    copy constructed
    int constructed
    move constructed
    === done ===
    

    我们正试图复制一个无限的重载函数序列,这些函数的行为如下 f1 ,这就是被拒绝的P1219R2会给我们的。不幸的是,唯一不需要额外副本的方法是 std::initializer_list<Tracer> ,在函数调用时需要一组额外的大括号。

    1 回复  |  直到 4 年前
        1
  •  6
  •   dfrib    4 年前

    为什么标准会施加这种限制?

    我将重点讨论“为什么”,因为其他答案已经访问了各种变通方法。

    P1219R2 ( 齐次变差函数参数 )一直到EWG

    # EWG incubator: in favor
    SF F N A SA
    5  2 3 0 0
    

    但是 was eventually rejected 用于EWG的C++23

    SF F N A SA
    2  8 8 9 2
    

    我认为其理由是,虽然该提案写得很好,但实际的语言功能并不是一个本质上有用的功能,尤其是不足以保持其影响力,因为这是由于C varargs逗号混乱造成的一个突破性变化:

    varargs省略号最初是在C++中与函数原型一起引入的。当时,该功能不允许在省略号之前使用逗号。当C后来采用这些特性时,语法被修改为需要插入逗号,强调最后一个形式参数和varargs参数之间的区别。为了保持与C的兼容性,对C++语法进行了修改,允许用户添加中间的逗号。因此,用户可以选择提供逗号或省略逗号。

    当与函数参数包配对时,这会产生当前通过消歧规则解决的语法歧义:当函数参数列表中出现的省略号可能是抽象(无名称)声明符的一部分时,如果参数的类型命名为未扩展的参数包或包含auto,则将其视为包声明;否则,它就是一个varargs省略号。目前,只要这样做会产生格式良好的结果,这个规则就会有效地消除歧义,支持参数包。

    示例(现状):

    template <class... T>
    void f(T...); // declares a variadic function template with a function parameter pack
    
    template <class T>
    void f(T...); // same as void f(T, ...)
    

    利用齐次函数参数包, 这个消歧规则 需要重新审视 。对 上面的第二个声明是具有同构的函数模板 函数参数包,这就是这里提出的解决方案。通过 参数列表和varargs省略号之间需要逗号 可以完全删除消歧规则,简化语言 而不会失去任何功能或降低与C的兼容性。

    这是一个突破性的变化 ,但可能影响不大。[…]

        2
  •  2
  •   user12002570    4 年前

    为什么标准会施加这种限制?

    很可能是因为这会将用户与旧用户混淆(或造成复杂性) C varargs函数 具有几乎相同的语法(带有未命名的参数),如下所示:

    //this is a C varargs function
    void good3(int...) 
               ^^^^^^
    {
        
    }
    

    现在如果第四个函数 bad 被允许:

     
    template<int = 0> void bad(int...i)
                               ^^^^^^^ --->this is very likely to confuse users as some may consider it as a C varargs function instead of a function parameter pack 
    {
    }
    

    IMO由于C varargs和函数参数包的语法相似,上述内容似乎至少有点模糊。

    从…起 dcl.fct#22 :

    有一个 句法歧义 当省略号出现在前面没有逗号的参数声明子句的末尾时。 在这种情况下,如果参数的类型命名了未扩展的模板参数包或包含auto,则省略号将被解析为抽象声明符的一部分;否则,它将作为参数声明子句的一部分进行解析。


    我想要一个当你说好的时候能工作的函数(1,2U,'\003')

    您可以使用 std::common_type 为此。

    template <typename... T, typename l= std::common_type_t<T...>>
    void func(T&&... args) {
    }
    

    我是不是错过了一些技巧 允许编写一个函数,该函数接受特定类型的可变数量的参数 ?

    这个 good3 你的例子中给出的是非常可读的,应该从C++20中使用。尽管如果使用C++17,那么一种方法是使用SFINAE原理和 std::conjunction std::is_same 如下所示。

    方法1

    在这里,我们只需检查是否所有传递的参数都是相同的类型。

    template <typename T, typename ... Args>
    std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
    func(const T & first, const Args & ... args)
    {
        std::cout<<"func called"<<std::endl;
        return true;
    }
    int main()
    {
        func(4,5,8);    //works
        //func(4,7.7); //wont work as types are different
        std::string s1 = "a", s2 = "b"; 
        func(s1, s2);  //works
        //func(s1, 2);   //won't work as types are different
        
    }
    

    Working demo


    看看你的评论,你似乎想再添加一个限制,即程序只能在

    a) 所有参数都属于同一类型

    b) 所有这些都与特定类型相匹配,比如 int std::string

    这可以通过添加 static_assert 在函数模板中:

    static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");
    

    方法2

    此处使用 静态断言 如上所示,以检查传递的参数是否为特定类型,如 int

    template <typename T, typename ... Args>
    std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
    func(const T & first, const Args & ... args)
    {
        static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");
        std::cout<<"func called"<<std::endl;
        return true;
    }
    
    int main()
    {
        func(3,3);    //works
        //func(4,7.7); //wont work as types are different
        std::string s1 = "a", s2 = "b"; 
        //func(s1, s2);  //won't work as even though they are of the same type but not int type
        
    }
    

    Working demo


        3
  •  1
  •   VainMan    4 年前

    我是不是错过了一些技巧,可以让一个人写一个函数,接受特定类型的可变数量的参数?

    如果我正确理解了这个问题,您可以通过添加一个代理函数来解决这个问题,该函数将参数强制转换并转发到实际实现中:

    unsigned wrap_sum(auto&&...args) {
      return []<std::size_t...I>(std::index_sequence<I...>,
                                 std::conditional_t<true, unsigned, decltype(I)> ... args) {
    
        return (0u + ... + args);
    
      }(std::make_index_sequence<sizeof...(args)>{},
        std::forward<decltype(args)>(args)...);
    }