C ++:自发性考古,为什么不应该使用C风格的变量函数

与往常一样,这一切都始于错误。 这是我第一次使用Java Native Interface,并且在C ++部分中,我包装了一个创建Java对象的函数。 该函数CallVoidMethod是可变的,即 除了指向JNI环境的指针,指向要创建的对象类型的指针以及被调用方法的标识符(在本例中为构造函数)之外,它还接受任意数量的其他参数。 这是合乎逻辑的,因为 这些其他参数传递给Java端的被调用方法,并且这些方法可以不同,具有任何类型的不同数量的参数。

因此,我还使包装器变量。 为了将任意数量的参数传递给CallVoidMethod使用了va_list ,因为在这种情况下它是不同的。 是的,这就是va_list发送到CallVoidMethod 。 并丢弃了JVM常规分段错误。

在两个小时内,我设法尝试了从第八版到第十一版的JVM的多个版本,因为:首先,这是我第一次使用JVM ,在这个问题上,我对StackOverflow的信任程度超过了我,其次,有人然后在StackOverflow上,我建议在这种情况下不要使用OpenJDK,而是使用OracleJDK,而不是8,而是10。只有到那时,我才最终注意到,除了变量CallVoidMethod之外,还有CallVoidMethodV ,它通过va_list接受任意数量的参数。

我最不喜欢这个故事的地方是,我没有立即注意到省略号(省略号)和va_list之间的区别。 在注意到之后,我无法向自己解释根本的区别是什么。 因此,我们需要处理省略号, va_list以及变量模板(因为我们仍在讨论C ++)。

标准中说了省略号和va_list呢?


C ++标准仅描述了其要求与标准C的要求之间的差异。稍后将讨论这些差异本身,但是现在,我将简要解释一下标准C所说的内容(从C89开始)。

  • 您可以声明一个带有任意数量参数的函数。 即 一个函数可以有比参数更多的参数。 为此,其参数列表必须以省略号结尾,但是还必须至少存在一个固定参数[C11 6.9.1 / 8]

     void foo(int parm1, int parm2, ...); 
  • 有关与省略号相对应的参数的数量和类型的信息不会传递给函数本身。 即 在最后一个命名参数(在上面的示例中为parm2 )之后[C11 6.7.6.3/9]
  • 要访问这些参数,必须使用在<stdarg.h>标头中声明的va_list类型和4个(在C11标准之前为3)宏: va_startva_argva_endva_copy (从C11开始) [C11 7.16]

    举个例子
     int add(int count, ...) { int result = 0; va_list args; va_start(args, count); for (int i = 0; i < count; ++i) { result += va_arg(args, int); } va_end(args); return result; } 

    是的,该函数不知道它有多少个参数。 她需要以某种方式传递这个数字。 在这种情况下,通过单个命名参数(另一个常见的选择是将NULL作为最后一个参数传递,如execl或0)。
  • 最后一个命名参数不能具有register存储类;它不能是函数或数组。 否则,未定义的行为[C11 7.16.1.4/4]
  • 而且,对于最后一个命名的自变量和所有无名的自变量,都应用了“ 默认自变量提升 ”( 默认自变量提升 ;如果将此概念很好地翻译成俄语,我很乐意使用它)。 这意味着,如果参数的类型为charshort (带或不带符号)或float ,则必须以intint (带或不带符号)或double形式访问相应的参数。 否则,未定义行为[C11 7.16.1.1/2]
  • 关于va_list类型,只说它是在<stdarg.h><stdarg.h>并且是完整的(也就是说,已知这种类型的对象的大小) [C11 7.16 / 3]

怎么了 但是因为!


C中没有很多类型。 为什么在标准中声明va_list ,但对其内部结构却什么也没说?

如果可以通过va_list传递函数的任意数量的参数,为什么需要省略号? 现在可以说:“作为语法糖”,但是40年前,我敢肯定,没有时间使用糖。

Philip James Plauger Phillip James Plauger《标准C库》 (1992年)一书中说,最初C是专门为PDP-11计算机创建的。 并且可以使用简单的指针算法对函数的所有参数进行排序。 问题随着C的普及以及将编译器转移到其他体系结构而出现。 Brian Kernighan和Dennis Ritchie于1978年出版的第一版C编程语言明确指出:
顺便说一句,没有一种可接受的方式来编写任意数量的参数的可移植函数,因为 对于被调用函数,没有可移植的方法来查找被调用时传递给它的参数数量。 ... printf是任意数量参数的最典型的C语言函数,...是不可移植的,必须为每个系统实现。
这本书介绍了printf ,但还没有vprintf ,也没有提到类型和宏va_* 。 它们出现在第二版的C编程语言(1988)中,这是开发第一个C标准(C89,又名ANSI C)的委员会的优点。 该委员会在标准中添加了<stdarg.h>标题,以Andrew Koenig为增加UNIX OS的可移植性而创建的<varargs.h>为基础。 决定将va_*宏保留为宏,以便现有编译器更容易支持新标准。

现在,随着C89和va_*系列的出现,创建可移植变量函数成为可能。 尽管这个家庭的内部结构仍然没有任何描述,也没有任何要求,但是很清楚为什么。

出于好奇,您可以找到<stdarg.h>的实现示例。 例如,相同的“ C标准库”提供了Borland Turbo C ++的示例:

来自Borland Turbo C ++的<stdarg.h>
 #ifndef _STADARG #define _STADARG #define _AUPBND 1 #define _ADNBND 1 typedef char* va_list #define va_arg(ap, T) \ (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND))) #define va_end(ap) \ (void)0 #define va_start(ap, A) \ (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND)) #define _Bnd(X, bnd) \ (sizeof(X) + (bnd) & ~(bnd)) #endif 


适用于AMD64的更新得多的SystemV ABI将此类型用于va_list

va_list SystemV ABI AMD64
 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1]; 


通常,可以说类型和宏va_*为遍历变量函数的参数提供了标准接口,并且出于历史原因,它们的实现取决于编译器,目标平台和体系结构。 此外,省略号(即通常为可变函数)早于va_list (即标头<stdarg.h> )出现在C中。 并不是创建va_list来代替省略号,而是使开发人员能够编写其可移植变量函数。

C ++很大程度上保持了与C的向后兼容性,因此上述所有内容均适用于C。 但是也有功能。

C ++中的变量函数


WG21工作组参与了C ++标准的开发。 1989年,以新创建的C89标准为基础,并逐渐更改以描述C ++本身。 1995年,从约翰· 米科( John Micco )收到了提案N0695 ,其中作者建议更改对宏va_*的限制:

  • 因为 与C不同,C ++允许您获取变量的register地址,然后变量函数的最后一个命名参数可以具有此存储类。
  • 因为 C ++中出现的链接违反了C变量函数的不成文规则-参数的大小必须匹配其声明的类型的大小-然后最后一个命名参数不能是链接。 否则,行为含糊。
  • 因为 在C ++中,没有“ 默认情况下提高参数类型 ”的概念,然后是
    如果参数parmN声明为...与在应用默认参数提升后产生的类型不兼容的类型,则该行为未定义
    必须替换为
    如果参数parmN声明为...与在传递没有参数的参数时导致的类型不兼容的类型,则行为未定义
为了分享我的痛苦,我什至没有翻译最后一点。 首先,C ++ Standard中的“ 默认参数类型升级 ”仍为[C ++ 17 8.2.2 / 9] 。 其次,与标准C相比,我很困惑这个短语的含义。 只有阅读了N0695,我才终于明白:我的意思是相同的。

但是,全部采用了3个更改[C ++ 98 18.7 / 3] 。 早在C ++中,对变量函数至少要具有一个命名参数的要求(在这种情况下,您将无法访问其他命名参数,但稍后再进行介绍)已经消失了,并且未命名参数的有效类型列表已添加了指向类成员和POD类型的指针。

C ++ 03标准未对变函数进行任何更改。 C ++ 11开始将类型为std::nullptr_t的未命名参数转换为void*并允许编译器自行决定使用非平凡的构造函数和析构函数来支持类型[C ++ 11 5.2.2 / 7] 。 C ++ 14允许使用函数和数组作为最后一个命名参数[C ++ 14 18.10 / 3] ,C ++ 17禁止使用由lambda捕获的扩展包和变量[C ++ 17 21.10.1 / 1]

结果,C ++在其陷阱中增加了可变函数。 只有非平凡的构造函数/析构函数的未指定类型支持才值得。 下面,我将尝试将变量函数的所有非显而易见的特性简化为一个列表,并通过具体示例对其进行补充。

如何轻松,错误地使用变量函数


  1. 用提升的类型声明最后一个命名的参数是不正确的,即 char ,有signed charunsigned charsinged shortunsigned shortfloat 。 根据标准的结果将是不确定的行为。

    无效的代码
     void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 


    在我手头的所有编译器(gcc,clang,MSVC)中,只有clang发出警告。

    lang警告
     ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    并且尽管在所有情况下编译后的代码都能正常运行,但您不应指望它。

    没错
     void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  2. 将最后一个命名的参数声明为引用是不正确的。 任何链接。 在这种情况下,该标准还保证了未定义的行为。

    无效的代码
     void foo(int& n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

    gcc 7.3.0编译了此代码,没有任何注释。 lang 6.0.0发出了警告,但仍进行了编译。

    lang警告
     ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^ 

    在这两种情况下,该程序都能正常运行(幸运的是,您不能依赖它)。 但是MSVC 19.15.26730表现出色-拒绝编译代码,因为 va_start参数不能为引用。

    来自MSVC的错误
     c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized 

    好吧,正确的选项看起来像这样
     void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  3. 请求va_arg引发char类型charshortfloat是错误的。

    无效的代码
     #include <cstdarg> #include <iostream> void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, float) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } int main() { foo(0, 1, 2.0f, 3); return 0; } 

    这里更有趣。 编译时的gcc发出警告,有必要使用double而不是float ,并且如果仍然执行此代码,则程序将以错误结尾。

    海湾合作委员会警告
     ./test.cpp:9:15: warning: 'float' is promoted to 'double' when passed through '...' std::cout << va_arg(va, float) << std::endl; ^~~~~~ ./test.cpp:9:15: note: (so you should pass 'double' not 'float' to 'va_arg') ./test.cpp:9:15: note: if this code is reached, the program will abort 

    实际上,该程序因抱怨无效指令而崩溃。
    转储分析表明该程序收到了SIGILL信号。 并且还显示了va_list的结构。 对于32位,这是

     va = 0xfffc6918 "" 

    va_list只是char* 。 对于64位:

     va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}} 

    即 完全与SystemV ABI AMD64中描述的相同。

    编译时发出的clang警告未定义的行为,还建议用double代替float

    lang警告
     ./test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] std::cout << va_arg(va, float) << std::endl; ^~~~~ 

    但是程序不再崩溃,32位版本产生:

     1 0 1073741824 

    64位:

     1 0 3 

    MSVC会产生完全相同的结果,即使使用/Wall也不会发出警告。

    这里可以假定32位和64位之间的差异是由于以下事实:在第一种情况下,ABI将所有参数通过堆栈传递给调用的函数,而在第二种情况下,前四个(Windows)或六个(Linux)参数通过处理器寄存器传递,其余的则通过堆栈[ Wiki ]。 但是不,如果您不使用4而是使用19来调用foo ,并以相同的方式输出它们,结果将是相同的:32位版本完全混乱,而64位一个float为零。 即 重点当然是在ABI中,而不是在使用寄存器传递参数时。

    好吧,当然,这样做是对的
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; std::cout << va_arg(va, double) << std::endl; std::cout << va_arg(va, int) << std::endl; va_end(va); } 

  4. 传递带有非平凡的构造函数或析构函数作为未命名参数的类的实例是不正确的。 当然,除非该代码的命运使您至少比“现在就编译并运行”更能激发您一点。

    无效的代码
     #include <cstdarg> #include <iostream> struct Bar { Bar() { std::cout << "Bar default ctor" << std::endl; } Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; } ~Bar() { std::cout << "Bar dtor" << std::endl; } }; struct Cafe { Cafe() { std::cout << "Cafe default ctor" << std::endl; } Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; } ~Cafe() { std::cout << "Cafe dtor" << std::endl; } }; void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto b = va_arg(va, Bar); va_end(va); } int main() { Bar b; Cafe c; foo(1, b, c); return 0; } 

    叮叮当当再次变得更加严格。 他只是拒绝编译此代码,因为第二个参数va_arg不是POD类型,并警告该程序将在启动时va_arg

    lang警告
     ./test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs] const auto b = va_arg(va, Bar); ^~~ ./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs] foo(1, b, c); ^ 

    如果仍然使用-Wno-non-pod-varargs标志进行编译,那么它将是这样。

    MSVC警告在这种情况下,将类型与非平凡的构造函数一起使用是不可移植的。

    来自MSVC的警告
     d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840:    "Bar"          

    但是代码可以编译并正确运行。 在控制台中获得以下内容:

    启动结果
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    即 仅在调用va_arg时创建一个副本,事实证明该参数是通过引用传递的。 虽然不明显,但标准允许。

    gcc 6.3.0编译时没有任何注释。 输出是相同的:

    启动结果
     Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor 

    gcc 7.3.0也不警告任何东西,但是行为正在改变:

    启动结果
     Bar default ctor Cafe default ctor Cafe copy ctor Bar copy ctor Before va_arg Bar copy ctor Bar dtor Bar dtor Cafe dtor Cafe dtor Bar dtor 

    即 此版本的编译器按值传递参数,并且在调用时, va_arg会创建另一个副本。 如果从构造函数/析构函数有副作用,则在从gcc的第6版切换到第7版时寻找这种差异会很有趣。

    顺便说一句,如果您显式传递并请求对该类的引用:

    另一个错误的代码
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto& b = va_arg(va, Bar&); va_end(va); } int main() { Bar b; Cafe c; foo(1, std::ref(b), c); return 0; } 

    那么所有编译器都会抛出错误。 按标准要求。

    通常,如果您确实愿意,最好通过指针传递参数。

    像这样
     void foo(int n, ...) { va_list va; va_start(va, n); std::cout << "Before va_arg" << std::endl; const auto* b = va_arg(va, Bar*); va_end(va); } int main() { Bar b; Cafe c; foo(1, &b, &c); return 0; } 


重载分辨率和变量函数


一方面,一切都很简单:即使在标准或用户定义的类型转换的情况下,与省略号的匹配也比与常规命名参数的匹配差。

过载示例
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo(int) { std::cout << "Ordinary function" << std::endl; } int main() { foo(1); foo(1ul); foo(); return 0; } 


启动结果
 $ ./test Ordinary function Ordinary function C variadic function 

但这仅在需要单独考虑对不带参数的foo的调用之前有效。

不带参数调用foo
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } int main() { foo(1); foo(); return 0; } 

编译器输出
 ./test.cpp:16:9: error: call of overloaded 'foo()' is ambiguous foo(); ^ ./test.cpp:3:6: note: candidate: void foo(...) void foo(...) ^~~ ./test.cpp:8:6: note: candidate: void foo() void foo() ^~~ 

一切都是按照标准进行的:没有参数-无法与省略号进行比较,并且当重载得到解决时,变量函数不会比平时的函数差。

但是什么时候值得使用变量函数


好吧,变异函数有时表现得不太明显,在C ++的上下文中,它们很容易移植。 Internet上有许多技巧,例如“请勿创建或使用可变C函数”,但是它们不会从C ++标准中删除其支持。 那么这些功能有一些好处吗? 好吧

  • 最常见和最明显的情况是向后兼容。 在这里,我将同时包括使用第三方C库(我在JNI中的情况)和为C ++实现提供C API。
  • SFINAE 。 在这里非常有用的是,在C ++中,变量函数不需要具有命名参数,并且在解析重载函数时,变量函数被认为是最后一个(如果存在至少一个参数)。 与其他函数一样,变量函数只能声明,而不能调用。

    例子
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static void detect(const U&); static int detect(...); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; }; 

    尽管在C ++ 14中,您可以做一些不同的事情。

    另一个例子
     template <class T> struct HasFoo { private: template <class U, class = decltype(std::declval<U>().foo())> static constexpr bool detect(const U*) { return true; } template <class U> static constexpr bool detect(...) { return false; } public: static constexpr bool value = detect<T>(nullptr); }; 

    在这种情况下,已经有必要注意观察哪些参数可以调用detect(...) 。 我宁愿更改几行代码,并使用现代方法替代变量函数,但要避免所有缺点。

变体模板或现代C ++中如何从任意数量的参数创建函数


Douglas Gregor,JaakkoJärvi和Gary Powell在2004年提出了变量模板的想法,即 在采用C ++ 11标准之前7年,正式支持这些变量模板。该标准包括其提案的第三版N2080

从一开始就创建了变量模板,以便程序员有机会从任意数量的参数中创建类型安全(可移植!)的函数。另一个目标是简化对具有可变数量参数的类模板的支持,但是现在我们只在谈论可变函数。

变量模板为C ++ [C ++ 17 17.5.3]带来了三个新概念

  • 模板参数包(模板参数包) -是一个参数模板,而不是它是可以传送的任何(包括0)模板参数的数量;
  • 一包函数参数(function parameter pack)-因此,这是一个可以接受任何数量(包括0)数量的函数参数的函数参数;
  • 包的扩展包扩展)是唯一可以使用参数包完成的操作。

例子
 template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); } 

class ... Args是模板参数Args ... args的包是功能参数的包,并且args...是功能参数的包的扩展。

在标准本身[C ++ 17 17.5.3 / 4]中给出了在何处以及如何扩展参数包的完整列表在讨论变量函数时,可以这样说:

  • 函数参数包可以扩展为另一个函数的参数列表
     template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); } 

  • 或到初始化列表
     template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; } 

  • 或到lambda捕获列表
     template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); } 

  • 可以在卷积表达式中扩展另一个函数参数包
     template <class ... Args> int foo(Args ... args) { return (0 + ... + args); } 

    卷积出现在C ++ 14中,可以是一元和二进制,也可以是左右。与往常一样,最完整的描述在Standard [C ++ 17 8.1.6]中
  • 两种类型的参数包都可以扩展为sizeof ...运算符
     template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); } 


在公开的明确省略号包需要支持各种模板(图案)公开并避免这种不确定性。

举个例子
 template <class ... Args> void foo() { using OneTuple = std::tuple<std::tuple<Args>...>; using NestTuple = std::tuple<std::tuple<Args...>>; } 

OneTuple-得出一个单例元组(std:tuple<std::tuple<int>>, std::tuple<double>>NestTuple的元组-一个包含一个元素的元组-另一个元组(std::tuple<std::tuple<int, double>>)。

使用变量模板的printf的示例实现


正如我已经提到的,还创建了变量模板,以直接替代C的变量函数。这些模板的作者自己提出了非常简单但类型安全的版本-C printf中最早的变量函数之一。

模板上的printf
 void printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout << *s++; } } template <typename T, typename ... Args> void printf(const char* s, T value, Args ... args) { while (*s) { if (*s == '%' && *++s != '%') { std::cout << value; return printf(++s, args...); } std::cout << *s++; } throw std::runtime_error("extra arguments provided to printf"); } 

我怀疑,然后通过递归调用重载函数,出现了这种可变参数枚举的模式。但是我仍然更喜欢没有递归的选项。

模板上的printf,没有递归
 template <typename ... Args> void printf(const std::string& fmt, const Args& ... args) { size_t fmtIndex = 0; size_t placeHolders = 0; auto printFmt = [&fmt, &fmtIndex, &placeHolders]() { for (; fmtIndex < fmt.size(); ++fmtIndex) { if (fmt[fmtIndex] != '%') std::cout << fmt[fmtIndex]; else if (++fmtIndex < fmt.size()) { if (fmt[fmtIndex] == '%') std::cout << '%'; else { ++fmtIndex; ++placeHolders; break; } } } }; ((printFmt(), std::cout << args), ..., (printFmt())); if (placeHolders < sizeof...(args)) throw std::runtime_error("extra arguments provided to printf"); if (placeHolders > sizeof...(args)) throw std::runtime_error("invalid format string: missing arguments"); } 

重载分辨率和可变模板功能


在解析时,这些可变函数在其他方面被认为是标准的和最不专门的。但是,没有参数的调用没有问题。

过载示例
 #include <iostream> void foo(int) { std::cout << "Ordinary function" << std::endl; } void foo() { std::cout << "Ordinary function without arguments" << std::endl; } template <class T> void foo(T) { std::cout << "Template function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); foo(2.0); foo(1, 2); return 0; } 

启动结果
 $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function 

解决重载后,变量模板函数只能绕过变量C函数(尽管为什么要混合它们?)。除了-当然!-不带参数的通话。

不带参数的通话
 #include <iostream> void foo(...) { std::cout << "C variadic function" << std::endl; } template <class ... Args> void foo(Args ...) { std::cout << "Template variadic function" << std::endl; } int main() { foo(1); foo(); return 0; } 

启动结果
 $ ./test Template variadic function C variadic function 

与省略号进行比较-相应的功能丢失,与省略号进行比较-模板功能低于非模板功能。

快速了解可变模板功能的速度


2008年,卢瓦克乔利(卢瓦克乔利)提交给标准化委员会C ++的提案N2772,这在实践中已经表明,模板函数的变化较慢类似的功能,这是初始化列表的参数(std::initializer_list)。尽管这与作者本人的理论依据相抵触,但Joli提出实施它std::minstd::max并且std::minmax是借助于初始化列表而不是变量模板。

但在2009年,已经出现了反驳。在Joli的测试中,发现了一个“严重错误”(甚至对他本人而言)。新测试(请参见此处此处)表明变量模板功能仍然更快,有时甚至更快。这并不奇怪,因为 初始化列表会复制其元素,对于变量模板,您可以在编译阶段进行大量操作。

然而,在C ++ 11和随后的标准std::minstd::max以及std::minmax-这是通常的模板功能,即通过初始化列表发送的参数的任意数量。

简要总结和结论


因此,C样式变量函数:

  • 他们不知道他们的论据数目或类型。开发人员必须使用该函数的部分参数,才能传递有关其余参数的信息。
  • 隐式提高未命名参数(和姓氏)的类型。如果您忘记了它,则会得到模糊的行为。
  • 它们保持与纯C的向后兼容性,因此不支持通过引用传递参数。
  • 在C ++ 11之前,不支持POD类型的参数,并且由于C ++ 11对非平凡类型的支持由编译器自行决定。 代码的行为取决于编译器及其版本。

变量函数的唯一允许用途是与C ++代码中的C API交互。对于其他所有东西,包括SFINAE,都有可变的模板函数,它们可以:

  • 了解他们所有参数的数量和类型。
  • 输入safe,请勿更改其参数的类型。
  • 它们支持以任何形式传递参数-按值,按指针,按引用,通过通用链接。
  • 像任何其他C ++函数一样,对参数类型没有任何限制。
  • ( C ), .

与C样式对应函数相比,可变模板函数可能更冗长,有时甚至需要使用自己的重载非模板版本(递归参数遍历)。他们很难阅读和写作。但是,由于没有列出的缺点和存在的优点,所有这些费用都可以得到补偿。

好了,结论很简单:仅由于向后兼容,C风格的变量函数仍保留在C ++中,并且它们提供了广泛的选择。在现代C ++中,强烈建议不要编写新的,如果可能的话,不要使用现有的可变C函数。可变模板函数属于现代C ++领域,并且更加安全。使用它们。

文献资料



聚苯乙烯


很容易找到和下载网上提到的书籍的电子版本。但是我不确定这是否合法,因此我不提供链接。

Source: https://habr.com/ru/post/zh-CN430064/


All Articles