【问题标题】:Why doesn't C++ support strongly typed ellipsis?为什么 C++ 不支持强类型省略号?
【发布时间】:2015-11-23 02:21:14
【问题描述】:

有人可以向我解释为什么 C++,至少据我所知,没有实现强类型省略号函数,效果如下:

void foo(double ...) {
 // Do Something
}

意思是,简单地说:'用户可以将可变数量的术语传递给 foo 函数,但是,所有术语都必须是双精度的'

【问题讨论】:

  • 我猜想可变参数函数被添加到 C 中的唯一目的是支持 printf 系列函数,它必须是类型不安全的。格式字符串 I/O 概念本身可能只是取自 C 的前辈,如 BCPL(参见 en.wikipedia.org/wiki/BCPL)。在现代 C++ 中,没有必要引入类型安全的可变参数函数,因为无论如何我们都有更好的语言结构,尤其是从 C++11 开始。不幸的是,我的猜测没有参考资料。向 Bjarne Stroustrup 本人提出这个问题会很有趣。
  • 您可以使用void foo(double *) 并通过foo((double[]){1,2,3,4,5}) 调用它。需要 GNU C++ 扩展。
  • @ChristianHackl:没有根本原因说明 printf 系列必须是类型不安全的。 C 可以声明实现首先将“类型令牌”压入调用堆栈,以便可变参数机制可以检查堆栈上是否存在正确的值类型。这会减慢正确代码的速度,而 C 历来强烈偏好快速而不是安全。
  • @MSalters:OTOH,在编译时仍然不能保证类型安全。
  • @user3528438 template<class T> using id = T; void foo(double*); foo(id<double[]>{1,2,3,4}); 无需扩展即可正常工作。

标签: c++ variadic-functions ellipsis


【解决方案1】:

 void foo(std::initializer_list<double> values);
 // foo( {1.5, 3.14, 2.7} );

非常接近。

您也可以使用可变参数模板,但它会变得更加散漫。至于真正的原因,我想说引入这种新语法的努力可能不值得:你如何访问单个元素?你怎么知道什么时候停止?是什么让它比std::initializer_list 更好?

C++ 确实有更接近于此的东西:非类型参数包。

template < non-type ... values>

喜欢

template <int ... Ints>
void foo()
{
     for (int i : {Ints...} )
         // do something with i
}

但是非类型模板参数(uhm)的类型有一些限制:比如不能是double

【讨论】:

  • 和非类型模板参数的值有限制——它们必须是编译时常量表达式。
【解决方案2】:

历史上,省略号语法...来自C。

这个复杂的野兽被用来为printf 类似的功能提供动力,并与va_listva_start 等一起使用......

正如您所说,它不是类型安全的;但是 C 远不是类型安全的,它对任何指针类型的隐式转换是从 void*void*,它对整数/浮点值的隐式截断等等......

因为 C++ 尽可能接近 C 的超集,所以它继承了 C 的省略号。


自诞生以来,C++ 实践不断发展,并且一直在大力推动更强大的类型化。

在 C++11 中,这最终导致:

  • 初始化器列表,给定类型的可变数量值的简写语法:foo({1, 2, 3, 4, 5})
  • 可变参数模板,它们本身就是一个野兽,例如允许编写类型安全的printf

可变参数模板实际上在其语法中重用省略号 ...,以表示类型或值的 并作为解包运算符:

void print(std::ostream&) {}

template <typename T, typename... Args>
void print(std::ostream& out, T const& t, Args const&... args) {
    print(out << t, args...); // recursive, unless there are no args left
                              // (in that case, it calls the first overload
                              // instead of recursing.)
}

注意... 的三种不同用法:

  • typename... 声明可变参数类型
  • Args const&amp;... 声明一组参数
  • args... 在表达式中解包

【讨论】:

  • print 的调用没有多大意义。 args 去哪儿了?
  • @Quentin:可变参数函数通常(今天)使用递归,所以你用 4 个参数调用 print,用 3 个参数调用 print,用 2 个参数调用 print,这使用 1 个参数调用 print => 这是基本情况(非可变函数)并且递归停止。
  • 我是盲人,没看到他们是print的两个重载。现在更有意义了:p
【解决方案3】:

可变参数模板和 SFINAE 已经可以实现:

template <bool...> struct bool_pack;
template <bool... v>
using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;

template <class... Doubles, class = std::enable_if_t<
    all_true<std::is_convertible<Doubles, double>{}...>{}
>>
void foo(Doubles... args) {}

感谢 Columbo 提供的 all_true 技巧。您还可以在 C++17 中使用折叠表达式。

随着以后和即将推出的标准都关注更简洁的语法(简洁的 for 循环、隐式函数模板......),您提出的语法很可能有一天会在标准中结束;)

【讨论】:

  • 我无法让它工作:error C2783: 'void foo(Doubles...)': could not deduce template argument for '&lt;unnamed-symbol&gt;'
  • @SimonKraemer IIRC SFINAE 在 MSVC 上出现各种故障,但遗憾的是我没有找到解决方法的经验......
  • 即使在 VS2015 中?好的....与此同时,我自己建立了一个“解决方法”:stackoverflow.com/a/32273936/4181011
  • @RyanHaining 哈哈,是的,C++ 通常从最完整的语法开始,然后为循环模式添加简写。如果您查看 C++03 迭代器 for 循环,它也非常庞大。
【解决方案4】:

我不知道为什么没有提出(或提出并拒绝)这样的事情。这样的事情肯定会有用,但会增加语言的复杂性。正如Quentin 演示的那样,已经提出了一种 C++11 方法来使用模板实现这样的事情。

当概念被添加到标准中时,我们将有另一种更简洁的方式:

template <Convertible<double>... Args>
void foo(Args... doubles);

template <typename... Args>
    requires Convertible<Args, double>()...
void foo(Args... doubles);

或者,如@dyp points out

void foo(Convertible<double>... doubles);    

就个人而言,在当前的解决方案和我们将通过概念画板获得的解决方案之间,我认为这是解决问题的充分解决方案。特别是因为最后一个基本上是你最初要求的。

【讨论】:

  • 或者只是void foo(Convertible&lt;double&gt;... doubles);(我必须查一下,但N4377保证可以工作;原因可能是参数-decl-clause中带有占位符的任何函数都是等效于 / 定义为函数模板)
  • @dyp 我个人不喜欢这种用法,因为现在我们隐瞒这是一个函数模板。虽然我期待通过回答有关它的问题得到所有代表:)
  • 哦,我们可以通过使用 pretty AllConvertibleToDouble{...T} void foo(T... t); 使 完全清楚 这是一个模板——我就是无法获得 gcc不幸的是接受AllConvertible&lt;double&gt;{...T} void foo(T... t); :( -- 嗯,现在我想定义一个名为Template的概念
  • @dyp AllConvertible&lt;double&gt;{...T} 是无效语法:模板介绍需要在 { 之前有一个限定概念名称。 AllConvertible&lt;double&gt; 是一个部分概念 ID。
  • 此外,FWIW,Convertible 可能会根据 LWG 在 7 月 Ranges TS 电话会议期间的指导命名为 ConvertibleTo。 (除非,显然,委员会再次更改名称;)
【解决方案5】:

实现(某种)您建议的方法是使用可变参数模板

template<typename... Arguments>
void foo(Arguments... parameters);

但是您现在可以在参数包中传递任何类型。 您提出的建议从未实施过,也许它可能是对语言的一个很好的补充,或者就目前的情况而言实施起来可能太难了。您可以随时尝试编写提案并将其提交给 isocpp.org

【讨论】:

  • 实现起来并不难。它实际上比可变参数模板更容易。然而,论文应该 (A) 说明为什么需要这样做,以及为什么 std::initializer_list&lt;double&gt; 不会这样做,并且可能 (B) 解释 sizeof... 如何为 double... 工作。 (C) 标准的实际建议措辞加分。
【解决方案6】:
template<typename T, typename... Arguments>
struct are_same;

template <typename T, typename A1, typename... Args>
struct are_same<T, A1, Args...>{    static const bool value = std::is_same<T, A1>::value && are_same<T, Args...>::value;};

template <typename T>
struct are_same<T>{static const bool value = true;};

template<typename T, typename... Arguments>
using requires_same = std::enable_if_t<are_same<T, Arguments...>::value>;

template <typename... Arguments, typename = requires_same<double, Arguments...>>
void foo(Arguments ... parameters)
{
}

【讨论】:

    【解决方案7】:

    基于Matthew's answer

    void foo () {}
    
    template <typename... Rest>
    void foo (double arg, Rest... rest)
    {
        /* do something with arg */
        foo(rest...);
    }
    

    如果使用foo 的代码编译,你知道所有的参数都可以转换为double

    【讨论】:

      【解决方案8】:

      因为你可以使用

      void foo(std::vector<T> values);
      

      【讨论】:

      • ...这是类型安全的,但只允许单一类型...而且调用起来也不是很“自然”。
      • 我严重怀疑这是不是这个原因。另外,这根本不是同一类东西。
      • @juan,请详细说明。 OP想要一个具有可变数量参数的函数的类似物,所有参数都是单一类型(明确说明)。现在,与此相比,我们这里有什么?数量不定的未命名参数?查看。能够计算实际传递的参数并获取它们的值吗?查看。当然,这不是原因,但是,我认为采用T向量的函数完全符合The user can pass a variable number of terms to the foo function, however, all of the terms must be of type T的OP要求
      • @SingerOfTheFall:对我来说,这听起来更像是 OP 想要一些历史参考或其他理论背景来说明为什么没有内置语言支持这样的功能。
      • @MSalters:嗯,这对我来说确实与foo(0.4, 0.3, 0.2); 非常不同。但无论如何感觉都是不合理的:)
      猜你喜欢
      • 2023-03-11
      • 2011-05-26
      • 2015-05-23
      • 1970-01-01
      • 1970-01-01
      • 2021-07-06
      相关资源
      最近更新 更多