值参数的常量正确性

33
我知道有一些问题涉及到const正确性,其中提到函数的声明和定义在值参数方面不需要保持一致。这是因为一个值参数的const性只在函数内部有意义。这没问题:
// header
int func(int i);

// cpp
int func(const int i) {
    return i;
}
这真的是最佳实践吗?因为我从来没有见过有人这么做。在其他讨论中,我看到过此引语(不确定来源):
“实际上,对于编译器而言,在值参数前面加上const还是不加都不会有任何区别。”
“避免在函数声明中使用传递方式为const值类型的参数。如果该参数不会被修改,在同一个函数的定义中仍然要将其设为const。”
第二段话说不要在声明中放const。 我认为这是因为值参数的const性质在接口定义中毫无意义。 它只是一种实现细节。
基于这个建议,对于指针参数的指针值也推荐这样吗?(在引用参数上是无意义的,因为您无法重新分配引用。)
// header
int func1(int* i);
int func2(int* i);

// cpp
int func1(int* i) {
    int x = 0;

    *i = 3; // compiles without error
    i = &x; // compiles without error

    return *i;
}
int func2(int* const i) {
    int x = 0;

    *i = 3; // compiles without error
    i = &x; // compile error

    return *i;
}

摘要:创建值参数有助于捕捉一些逻辑错误。这是最佳实践吗?你是否会过分地在头文件中省略const?使用const指针值是否同样有用?为什么?

一些参考文献:

C++ const 关键字-要慷慨使用吗? 函数参数使用const的用法

当const值参数有用的一个例子:

bool are_ints_equal(const int i, const int j) {
    if (i = j) { // without the consts this would compile without error
        return true;
    } else {
        return false;
    }
    // return i = j; // I know it can be shortened
}

它在这种情况下捕获了“if(i = j)”错误,但并不捕获所有这样的错误,因此我不会对特定的理由感到太兴奋(因为您可以使用变量犯同样的错误)。即使没有const,如果您告诉编译器您想要警告,它也应该向您发出警告。 - Brent Bradburn
通过 if (i = j) 感到兴奋的重点在于意识到 const 值参数并不只是花哨的东西。Michael Burr 的例子甚至比这个更好。 - jmucchiello
2
在您不更改函数参数的情况下,应该将它们声明为const,因为A)这样更安全,B)这样更易于理解,C)这样更便于调试。此外,原型和头文件也应标记为const。如果只在函数头中完成,则会令人困惑。在函数内部创建临时变量的论点是一种情况,您可能不需要声明参数为const。这是我的意见。 - user898058
6个回答

14
我曾多次阅读到将函数中的值参数设为const是一件不必要的坏事情。然而,作为实现者,我偶尔会发现它对我有所帮助,因为它可以检查我的实现是否执行我想要的操作(就像在您问题的结尾处所示的示例中)。 因此,虽然它可能对调用者没有任何帮助,但有时对我作为实现者来说确实增加了一点价值,而且并不会削弱调用者的功能。 所以我认为使用它没有什么害处。例如,我可能正在实现一个C函数,该函数需要指向缓冲区开头和结尾的几个指针。我将在缓冲区中放置数据,但希望确保不会超出结尾。因此,在函数内部,会有代码在我添加数据时递增指针。 将指向缓冲区结尾的指针设为const参数将确保我不会编写代码错误地递增结束边界指针,而应正确递增指针。因此,fillArray签名的函数可能如下所示:
size_t fillArray( data_t* pStart, data_t* const pEnd);

使用该方法能够防止我在实际想要增加pStart时,错误地增加pEnd。这并不是什么大事,但我相信所有长期编写C代码的程序员都曾遇到过这种错误。


14

我的看法:

这不是一个坏主意,但问题很小,你的精力可能会更好地花在其他事情上。

在你的问题中,你提供了一个很好的例子,说明它何时可以捕捉到错误,但有时你也会做出像这样的事情:

void foo(const int count /* … */)
{
   int temp = count;  // can't modify count, so we need a copy of it
   ++temp;

   /* … */
}

无论哪种方式,优缺点都很小。


3
赞同说这不是一个坏主意,并指出问题很小,但在复制参数之前,我会删除const(除非您总是复制“count”作为不操作参数的原则,而不仅仅是因为const会阻止您这样做)。 - Johannes Schaub - litb
8
通常当你像这样修改变量时,会改变其语义含义。例如:void foo(const int index0based){const int index1based = index0based + 1;} 你可以将其称为“index”并进行更改,但对于未来5年后的维护程序员来说,这种方式更加清晰易懂。 - Bill
比尔- 我的例子是人为的,但你可能对许多情况都是正确的。维护程序员的问题是我非常关心的事情 ;) - luke
1
实际上,我看过类似于你的示例代码。一个项目计数被传递进来,而不是创建一个本地的 int currentCount = count;,参数被使用并在函数执行期间进行调整...非常令人困惑! - Bill

1

不幸的是,一些编译器(我在看你,Sun CC!)错误地区分已声明为const和未声明为const的参数,这可能会导致关于未定义函数的错误。


0

我认为这取决于你的个人风格。

它不会增加或减少客户可以传递给函数的内容。本质上,它就像一个编译时断言。如果它能帮助你知道值不会改变,那就去做吧,但我不认为其他人有很大的理由去这样做。

我可能不这样做的一个原因是,值参数的const属性是客户不需要知道的实现细节。如果您稍后(故意)更改函数以实际更改该值,则需要更改函数的签名,这将迫使客户重新编译。

这类似于为什么有些人建议没有公共虚拟方法(函数的虚拟性是应该对客户隐藏的实现细节),但我不属于这个特定的阵营。


3
请重新阅读问题。头文件中没有const,因为正如您所说,调用者不关心实现是否修改了局部变量。这个问题严格来说是关于在实现方面是否值得这样做的问题。 - jmucchiello

0

如果存在const关键字,则意味着'i'的值(即const类型)不能被修改。 如果在foo函数内更改'i'的值,编译器将抛出错误:

无法修改const对象

但是更改'*i'(即*i = 3;)意味着您并没有更改'i'的值,而是更改了'i'指向的地址的值

实际上,const函数适用于不应由函数更改的大型对象。


-2
我喜欢const correctness这样的情况:
void foo(const Bar &b) //我知道b不能被改变
{
//对b做一些处理
}

这让我可以使用b而不必担心修改它,但我不必支付复制构造函数的成本。


11
我们不讨论引用参数,仅涉及值参数。 - jmucchiello

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