【问题标题】:C++ function argument safetyC++ 函数参数安全
【发布时间】:2016-12-14 16:05:27
【问题描述】:

在一个接受多个相同类型参数的函数中,我们如何保证调用者不会弄乱顺序?

例如

void allocate_things(int num_buffers, int pages_per_buffer, int default_value ...

以后

// uhmm.. lets see which was which uhh..
allocate_things(40,22,80,...

【问题讨论】:

  • 编译器可以在大多数情况下为您提供帮助。否则,这是你(程序员)的责任。
  • 在 C++ 中使用特定类型不是很简单吗?
  • 你能用method chaining吗?类似allocate_thing().buffers(40).pages_per_buffer(22).default_value(80)
  • 这是个好问题。我认为唯一真正的解决方案是为需要配置的每个项目创建值类型。就像 <chrono> 库使用 durations 例如 std::chrono::seconds 来配置时间段。
  • @gnasher - 同意,这是一个危险的功能 - 这使它成为一个特别的例子。

标签: c++ c++14


【解决方案1】:

一个典型的解决方案是将参数放在一个结构中,并带有命名字段。

AllocateParams p;
p.num_buffers = 1;
p.pages_per_buffer = 10;
p.default_value = 93;
allocate_things(p);

当然,您不必使用字段。你可以使用成员函数或任何你喜欢的东西。

【讨论】:

  • @FrankPuffer:是的,同意,但这不是代码审查堆栈交换。如果您对原始作者的代码有 cmets,它们属于问题的 cmets,而不是答案。此代码示例旨在说明一种特定的技术,仅此而已。
  • @FrankPuffer:我认为很明显这些只是占位符名称。
  • @Galik 使用这种模式,程序员必须睡得更久才能弄错,因为他们需要按名称引用字段。 (直到他们忘记了为什么这样做,并认为通过带括号的初始化列表很聪明,以原始问题结束 + 新的无意义的填充符 [edit: nate,我们又做了一次])
  • @Galik 即allocate_things({ 1, 10, 93 });
  • @FrankPuffer:我认为很明显这不应该是一个真正的功能。您断言该函数“做了太多事情”基本上是没有根据的-您拥有的唯一信息是函数名称,这显然是虚构的!也可能是foo()。这种不切实际的评论是我对 Stack Overflow 的最大挫败感。
【解决方案2】:

如果您有 C++11 编译器,则可以将 user-defined literals 与用户定义的类型结合使用。这是一个幼稚的方法:

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

【讨论】:

  • ...哇哦。 +1;这非常很有趣。但我不知道我是否想要找到我需要它的场景...... ;-)
  • 这个好像可以宏化了。
  • 如果 40 是一个变量而不是一个字面量呢?
  • @Barry 我猜如果 40 是一个变量,它会有一个有意义的名称。 operator"" 不会被使用。
  • @Joker_vD:用户定义的文字后缀是相反的。 _ 开头的后缀被保留。 (C++11 §17.6.4.3.5;没有后续版本的部分。)
【解决方案3】:

到目前为止,有两个很好的答案,还有一个:另一种方法是尽可能利用类型系统,并创建强大的 typedef。例如,使用 boost strong typedef (http://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html)。

BOOST_STRONG_TYPEDEF(int , num_buffers);
BOOST_STRONG_TYPEDEF(int , num_pages);

void func(num_buffers b, num_pages p);

使用错误顺序的参数调用 func 现在会出现编译错误。

对此有几点说明。首先,boost 的强 typedef 在其方法上相当过时;您可以使用可变参数 CRTP 做更好的事情并完全避免使用宏。其次,显然这会引入一些开销,因为您经常必须显式转换。所以通常你不想过度使用它。对于您的图书馆中一遍又一遍地出现的东西来说,这真是太好了。对于一次性出现的事情不太好。例如,如果你正在编写一个 GPS 库,你应该有一个以米为单位的距离的强 double typedef,一个以纳秒为单位的强 int64 typedef,等等。

【讨论】:

  • 特别是对于整数,范围枚举是一个不错的选择。
  • 您可以更进一步地使用这种方法,使用用户定义的文字来减少拨打电话时使用海关类型的语法开销。
  • 你会得到一个看起来像allocate_things(40_buffers,22_pages, 80...的调用,如果你没有把值放在正确的地方,它会给你一个编译器错误。
【解决方案4】:

(注意:帖子最初标记为'C`)

C99 及更高版本允许扩展至 @Dietrich Epp 想法:复合文字

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

甚至可以传递结构的地址。

allocate_things(struct things *);

// Use a compound literal
allocate_things(&((struct things){.default_value=80,.num_buffers=40,.pages_per_buffer=22}));

【讨论】:

  • 但这是关于 C++ 的。它不会从 C 中导入复合文字。
  • @underscore_d 这篇文章在编辑之前关于 C 的。 (这篇文章在 C 上下文中仍然有意义 - 不清楚 OP/πάντα ῥεῖ 更改。 - 现在看到它与标题相关)
  • 是的,刚刚看到。根据原始标签公平竞争。虽然标题总是不同意。如果只有人们会标记他们真正的意思......叹息
  • 不要使用指针,使用引用。使用指针意味着函数必须处理nullptr 的情况,而使用引用需要对象存在。同样,现在一般建议是避免使用指针并改用智能指针
  • @Pharap Post 最初被标记为 C,这个答案与此相关,因此您的参考想法对 C++ 有好处。 OP 的帖子已经删除了C 标签。
【解决方案5】:

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

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

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

【讨论】:

  • 并分离职责。
  • 并且不要使用幻数,像你这样硬编码参数通常会导致比它的价值更多的痛苦。
  • 这会为系统引入不必要的(可能是全局的)状态
  • @nate 函数算作“状态”吗?我一定错过了那份备忘录。还是您的意思是,为以后可能需要交互的属性设置单独的函数意味着在设置它们的过程中需要存储它们?
  • 为了让set_XXX 影响未来的allocate_things 调用,参数必须存储在某个地方。
【解决方案6】:

为了完整起见,您可以在调用时使用命名参数

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++,这需要执行相当多的代码(在声明 allocate_things() 的头文件中,还必须声明适当的外部对象 num_buffers 等提供 operator=,它返回一个唯一的合适对象)。

--------- 工作示例(用于 sergej)

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

【讨论】:

  • 看起来像 C++35 左右... +1。希望看到一个最小的工作示例。
【解决方案7】:

您真的要尝试 QA 任意整数的所有组合吗?并进行所有负值/零值等检查?

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

allocate_things(MINIMUM_BUFFER_CONFIGURATION, LARGE_BUFFER_SIZE, 42);

然后,您只需测试有限数量的组合,即可获得 100% 的覆盖率。 5 年后,编写您的代码的人员只需要知道他们想要实现的目标,而不必猜测他们可能需要的数字或实际测试过的值。

这确实使代码更难扩展,但听起来参数是用于低级性能调整的,因此不应将调整值视为廉价/琐碎/不需要彻底测试。对变更的代码审查 allocate_something(25, 25, 25);

...到

allocate_something(30, 80, 42);

...可能只是耸耸肩/吹嘘,但对新枚举值 EXTRA_LARGE_BUFFERS 的代码审查可能会引发关于内存使用、文档、性能测试等的所有正确讨论。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-03-23
    • 2018-10-25
    • 2013-10-11
    • 1970-01-01
    • 2010-10-04
    • 1970-01-01
    • 1970-01-01
    • 2011-02-28
    相关资源
    最近更新 更多