关于c ++ 14:C ++函数参数安全性

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);
}


不能。这就是为什么建议尽可能少地使用函数参数的原因。

在您的示例中,可以有单独的函数,如set_num_buffers(int num_buffers)set_pages_per_buffer(int pages_per_buffer)等。

您可能已经注意到,allocate_things不是一个好名字,因为它不表示函数实际在做什么。尤其是我不希望它设置默认值。


您真的要尝试对任意整数的所有组合进行质量保证吗?把所有的负/零值检查都扔进去?

只需为最小、中等和最大缓冲区数量以及较小、中等和较大缓冲区大小创建两个枚举类型。然后让编译器完成工作,让您的QA人员休息一个下午:

1
allocate_things(MINIMUM_BUFFER_CONFIGURATION, LARGE_BUFFER_SIZE, 42);

然后你只需要测试有限数量的组合,你就可以得到100%的覆盖率。从现在开始5年后,编写代码的人员只需要知道他们想要实现什么,而不必猜测他们可能需要的数字,或者哪些值已经在字段中进行了实际测试。

它确实使代码的扩展稍微困难一些,但听起来参数是用于低级性能调优的,因此不应认为扭曲值是便宜的/微不足道的/不需要彻底测试的。对更改的代码审阅分配某物(25,25,25);

……

分配某物(30,80,42);

…可能只是耸耸肩/发泄,但是对新的枚举值超大缓冲区的代码审查可能会触发关于内存使用、文档、性能测试等的所有正确讨论。