通过参数返回值是一个好的设计吗?

24
bool is_something_ok(int param,SomeStruct* p)
{
    bool is_ok = false;

    // check if is_ok

    if(is_ok)
       // set p to some valid value
    else
       // set p to NULL
    return is_ok;
}

如果“某事情正常”,这个函数会返回true并设置p为有效值,否则返回false并将p设置为NULL。

这是一个好的设计还是不好的设计?就我个人而言,使用它时我感到不舒服。如果没有文档和注释,我真的不知道该如何使用它。

顺便问一下:是否有关于API设计的权威书籍/文章?


7
请注意,该函数无法将指针设置为NULL。函数参数传递的是指针的副本,即使该副本被设为null,调用该函数的代码也不会注意到。如果您想要更改指针,则需要额外的间接级别(可以称其为指针或引用:bool op(int, SomeStruct *& )bool op(int, SomeStruct ** )),这样情况就变得有点棘手了。 - David Rodríguez - dribeas
你可能想看一下这个答案:https://dev59.com/lnI95IYBdhLWcg3w1BdN - sbi
9个回答

16

既然你将问题标记为C++而不是C,我建议你:

  • 直接返回值
  • 如果有多个返回值,请使用输出参数
  • 尽可能使用非const引用作为输出参数(而不是指针),并对输入参数使用const引用。
  • 如果出现问题,请抛出异常而不是返回false或-1。

这些只是一些通用的提示。具体问题的最佳解决方法总是取决于特定情况...


如果你只有一个返回值,最好的做法是什么?我猜直接返回它就可以了。 - stephanmg

3

这取决于您想如何处理“错误”。

例如,以标准函数atoi为例。它将字符串转换为整数,但如果该字符串不包含数字,应返回什么呢?在这种情况下,C/C++运行时会设置errno全局变量。另一种选择是抛出异常。

个人而言,我并不喜欢这两种替代方案。因此,通常我遵循以下规则:

  • 是否有可能的返回值范围中存在可以用于指示错误的值,并且我只有1种可能的“错误类型”?在这些情况下,我使用返回值来指示错误。例如,像FindEmployee这样的函数如果找不到员工,可以简单地返回NULL。
  • 如果函数可以返回所有可能的值(如atoi示例中),则使用输出参数作为返回值,并让函数返回一个布尔值。如果您有多个可能的错误情况,请返回指示发生的错误(或成功)类型的枚举。

第三个选项(在QT中使用)是将错误返回变量作为参数传递(int QString :: toInt(bool * error = 0,int base = 0))。讨论其他错误机制会额外加分。 - David Rodríguez - dribeas

2
我认为这要看情况而定。你的类型复制构造的成本有多高?你能写一个友好的RVO函数吗?至少在我们拥有C++0x的右值引用和移动语义之前,我建议不要返回“昂贵”的类型(如std::vector<std::string>),而是将它们作为引用传递,例如使用:

void split(const std::string &txt, char sep, std::vector<std::string> &out);

不是:

std::vector<std::string> split(const std::string &txt, char sep);

根据你编写函数的方式,可以触发RVO优化,但在我的经验中,这并不是通常可以依靠的。


2
我倾向于这样做。在你的例子中,另一种选择是将两个东西编码为一个返回值(例如使用NULL作为特殊值),或者返回一个结构体。
将两个东西编码有时是不可能的,并且容易出错。返回结构体会增加很多额外的工作和混乱。所以我倾向于像你一样做。我倾向于假设参数列表中的“原始”指针和引用是用于返回值的,如果它们仅用于传递数据,则应该为“const”。
但说实话,我忘记这条规则的次数和记住它的次数一样频繁,所以也许这不是一个非常好的规则。
boost库中有一个叫做“optional”的类,它可能符合您的需求,但我自己从来没有真正喜欢过它,也许没有什么很好的理由。

+1 因为将可选项带入讨论中。这是一个几乎总被忘记的小巧实用工具... - David Rodríguez - dribeas

2
关于您关于API设计书籍的问题。请查找Martin Reddy于2011年出版的《API Design for C++》。
对于已接受答案的评论。在该书中,作者实际上建议优先选择const引用作为输入参数,并选择指针作为输出参数,因为这更明确地告诉客户端参数可能会被修改,例如foo(bar) vs. foo(&bar)。
此外,您可以观看演讲How To Design A Good API and Why it Matters。其中主要使用Java,但我记得。

1

我认为在这种情况下,如果出现问题并且无效,则返回NULL,如果一切正常,则返回SomeStruct更好。

SomeStruct* is_something_ok(int param);

在这种情况下,除了检查布尔值之外,您还应该检查它是否为NULL,如果不是,则使用它。
但是,在某些情况下,您必须通过参数返回值。这取决于返回值的数量和上下文函数的使用方式。

但有时候NULL确实是一个有效的返回值。 - Matthew Flaschen
在这种情况下,您可以定义类的特定实现来代表空值。然后,调用代码可以不加区别地处理两种情况。 - Nate W.
1
如果你在团队中工作,保持简单可能更好,因为这会使设计更加复杂。 - Phong
1
在某些情况下,当NULL是有效值或返回类型不能为NULL时,Microsoft在.NET框架中选择将返回值作为参数,例如TryParse方法。 - Arsen Mkrtchyan

1
你可以做以下事情:
bool is_something_ok(int param,SomeStruct* p);

或者

// return NULL if the operation failed.
Somestruct* do_work(int param);

很难说一个设计/API 是好还是坏,没有绝对的黑白之分...(灰色?!?)

你必须选择那些更容易编码的API/标准,并且要保持一致性,如果你选择了第一种方法类型,在整个项目中都要使用它。

同时,不要忘记记录你的代码,这样别人就更容易理解如何使用你的API。


1
我建议直接返回结果类型,像这样:


SomeStruct doSomething(int param) {...}

在函数无法处理的情况下抛出异常(tux21b已经提到了这种方式)。或者,您可以使用std::pair返回两种类型,而不会抛出异常,如下所示:

pair<SomeStruct, bool> doSomething(int param) {...}

第三,我喜欢将输出参数声明为指针而不是引用(就像您提到的那样),因为在调用代码中我可以看到输入和输出参数的区别。给定函数:

void doSomething(const Somestruct& in, Somestruct* out) {...}

然后在调用代码中,即使不查看函数声明,也可以看到输入和输出参数是什么(如果我一致地应用这个概念)。

SomeStruct a;
SomeStruct b;
doSomething(a, &b); // you see a is input, b is output

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接