C++ function argument safety
在一个接受同一类型的几个参数的函数中,我们如何保证调用方不会弄乱顺序呢?
例如
1 | void allocate_things(int num_buffers, int pages_per_buffer, int default_value ... |
以后
1 2 | // uhmm.. lets see which was which uhh.. allocate_things(40,22,80,... |
一个典型的解决方案是将参数放在具有命名字段的结构中。
1 2 3 4 5 | AllocateParams p; p.num_buffers = 1; p.pages_per_buffer = 10; p.default_value = 93; allocate_things(p); |
当然,你不必使用字段。您可以使用成员函数或任何您喜欢的函数。
如果你有一个C++ 11编译器,你可以使用用户定义的文字和用户定义的类型。以下是一个幼稚的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | struct num_buffers_t { constexpr num_buffers_t(int n) : n(n) {} // constexpr constructor requires C++14 int n; }; struct pages_per_buffer_t { constexpr pages_per_buffer_t(int n) : n(n) {} int n; }; constexpr num_buffers_t operator"" _buffers(unsigned long long int n) { return num_buffers_t(n); } constexpr pages_per_buffer_t operator"" _pages_per_buffer(unsigned long long int n) { return pages_per_buffer_t(n); } void allocate_things(num_buffers_t num_buffers, pages_per_buffer_t pages_per_buffer) { // do stuff... } template <typename S, typename T> void allocate_things(S, T) = delete; // forbid calling with other types, eg. integer literals int main() { // now we see which is which ... allocate_things(40_buffers, 22_pages_per_buffer); // the following does not compile (see the 'deleted' function): // allocate_things(40, 22); // allocate_things(40, 22_pages_per_buffer); // allocate_things(22_pages_per_buffer, 40_buffers); } |
到目前为止,还有两个很好的答案:另一种方法是尽可能地利用类型系统,并创建强typedef。例如,使用boost-strong typedef(http://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html)。
1 2 3 4 | BOOST_STRONG_TYPEDEF(int , num_buffers); BOOST_STRONG_TYPEDEF(int , num_pages); void func(num_buffers b, num_pages p); |
使用错误的参数顺序调用func现在将是一个编译错误。
关于这个的几条注释。首先,Boost的强typedef在其方法中相当过时;您可以使用variadic crtp做更好的事情,并且完全避免使用宏。第二,显然这会引入一些开销,因为您经常需要显式转换。所以一般来说你不想过度使用它。在图书馆里反复出现的东西真是太好了。不太适合一个人的事情。例如,如果您正在编写一个GPS库,那么您应该有一个以米为单位的强双typedef,一个以纳秒为单位的强Int64 typedef,等等。
(注:post最初被标记为"c")。
c99以后允许对@dietrich epp idea:compound literal进行扩展
1 2 3 4 5 6 7 8 9 | struct things { int num_buffers; int pages_per_buffer; int default_value }; allocate_things(struct things); // Use a compound literal allocate_things((struct things){.default_value=80, .num_buffers=40, .pages_per_buffer=22}); |
甚至可以传递结构的地址。
1 2 3 4 | allocate_things(struct things *); // Use a compound literal allocate_things(&((struct things){.default_value=80,.num_buffers=40,.pages_per_buffer=22})); |
为了完整性起见,可以在调用变为时使用命名参数。
1 2 3 | void allocate_things(num_buffers=20, pages_per_buffer=40, default_value=20); // or equivalently void allocate_things(pages_per_buffer=40, default_value=20, num_buffers=20); |
但是,在当前C++中,这需要相当多的代码来实现(在报头文件中声明EDOCX1)3,它还必须声明适当的外部对象EDCOX1(4)等,提供EDCOX1×5),返回一个唯一合适的对象。
-------工作示例(用于Sergej)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <iostream> struct a_t { int x=0; a_t(int i): x(i){} }; struct b_t { int x=0; b_t(int i): x(i){} }; struct c_t { int x=0; c_t(int i): x(i){} }; // implement using all possible permutations of the arguments. // for many more argumentes better use a varidadic template. void func(a_t a, b_t b, c_t c) { std::cout<<"a="<<a.x<<" b="<<b.x<<" c="<<c.x<<std::endl; } inline void func(b_t b, c_t c, a_t a) { func(a,b,c); } inline void func(c_t c, a_t a, b_t b) { func(a,b,c); } inline void func(a_t a, c_t c, b_t b) { func(a,b,c); } inline void func(c_t c, b_t b, a_t a) { func(a,b,c); } inline void func(b_t b, a_t a, c_t c) { func(a,b,c); } struct make_a { a_t operator=(int i) { return {i}; } } a; struct make_b { b_t operator=(int i) { return {i}; } } b; struct make_c { c_t operator=(int i) { return {i}; } } c; int main() { func(b=2, c=10, a=42); } |
不能。这就是为什么建议尽可能少地使用函数参数的原因。
在您的示例中,可以有单独的函数,如
您可能已经注意到,
您真的要尝试对任意整数的所有组合进行质量保证吗?把所有的负/零值检查都扔进去?
只需为最小、中等和最大缓冲区数量以及较小、中等和较大缓冲区大小创建两个枚举类型。然后让编译器完成工作,让您的QA人员休息一个下午:
1 | allocate_things(MINIMUM_BUFFER_CONFIGURATION, LARGE_BUFFER_SIZE, 42); |
然后你只需要测试有限数量的组合,你就可以得到100%的覆盖率。从现在开始5年后,编写代码的人员只需要知道他们想要实现什么,而不必猜测他们可能需要的数字,或者哪些值已经在字段中进行了实际测试。
它确实使代码的扩展稍微困难一些,但听起来参数是用于低级性能调优的,因此不应认为扭曲值是便宜的/微不足道的/不需要彻底测试的。对更改的代码审阅分配某物(25,25,25);
……
分配某物(30,80,42);
…可能只是耸耸肩/发泄,但是对新的枚举值超大缓冲区的代码审查可能会触发关于内存使用、文档、性能测试等的所有正确讨论。