开发者

Does it ever make sense to make a fundamental (non-pointer) parameter const?

开发者 https://www.devze.com 2022-12-22 20:59 出处:网络
I recently had an exchange with another C++ developer about the following use of const: void Foo(const int bar);

I recently had an exchange with another C++ developer about the following use of const:

void Foo(const int bar);

He felt that using const in this way was good practice.

I argued that it does nothing for the caller of the function (since a copy of the argument was going to be passed, there i开发者_如何学Pythons no additional guarantee of safety with regard to overwrite). In addition, doing this prevents the implementer of Foo from modifying their private copy of the argument. So, it both mandates and advertises an implementation detail.

Not the end of the world, but certainly not something to be recommended as good practice.

I'm curious as to what others think on this issue.

Edit:

OK, I didn't realize that const-ness of the arguments didn't factor into the signature of the function. So, it is possible to mark the arguments as const in the implementation (.cpp), and not in the header (.h) - and the compiler is fine with that. That being the case, I guess the policy should be the same for making local variables const.

One could make the argument that having different looking signatures in the header and source file would confuse others (as it would have confused me). While I try to follow the Principle of Least Astonishment with whatever I write, I guess it's reasonable to expect developers to recognize this as legal and useful.


Remember the if(NULL == p) pattern ?

There are a lot of people who will tell a "you must write code like this":

if(NULL == myPointer) { /* etc. */ }

instead of

if(myPointer == NULL) { /* etc. */ }

The rationale is that the first version will protect the coder from code typos like replacing "==" with "=" (because it is forbidden to assign a value to a constant value).

The following can then be considered an extension of this limited if(NULL == p) pattern:

Why const-ing params can be useful for the coder

No matter the type, "const" is a qualifier that I add to say to the compiler that "I don't expect the value to change, so send me a compiler error message should I lie".

For example, this kind of code will show when the compiler can help me:

void bar_const(const int & param) ;
void bar_non_const(int & param) ;

void foo(const int param)
{
   const int value = getValue() ;

   if(param == 25) { /* Etc. */ } // Ok
   if(value == 25) { /* Etc. */ } // Ok

   if(param = 25) { /* Etc. */ } // COMPILE ERROR
   if(value = 25) { /* Etc. */ } // COMPILE ERROR

   bar_const(param) ;  // Ok
   bar_const(value) ;  // Ok

   bar_non_const(param) ;  // COMPILE ERROR
   bar_non_const(value) ;  // COMPILE ERROR

   // Here, I expect to continue to use "param" and "value" with
   // their original values, so having some random code or error
   // change it would be a runtime error...
}

In those cases, which can happen either by code typo or some mistake in function call, will be caught by the compiler, which is a good thing.

Why it is not important for the user

It happens that:

void foo(const int param) ;

and:

void foo(int param) ;

have the same signature.

This is a good thing, because, if the function implementer decides a parameter is considered const inside the function, the user should not, and does not need to know it.

This explains why my functions declarations to the users omit the const:

void bar(int param, const char * p) ;

to keep the declaration as clear as possible, while my function definition adds it as much as possible:

void bar(const int param, const char * const p)
{
   // etc.
}

to make my code as robust as possible.

Why in the real world, it could break

I was bitten by my pattern, though.

On some broken compiler that will remain anonymous (whose name starts with "Sol" and ends with "aris CC"), the two signatures above can be considered as different (depending on context), and thus, the runtime link will perhaps fail.

As the project was compiled on a Unix platforms too (Linux and Solaris), on those platforms, undefined symbols were left to be resolved at execution, which provoked a runtime error in the middle of the execution of the process.

So, because I had to support the said compiler, I ended polluting even my headers with consted prototypes.

But I still nevertheless consider this pattern of adding const in the function definition a good one.

Note: Sun Microsystems even had the balls to hide their broken mangling with an "it is evil pattern anyway so you should not use it" declaration. see http://docs.oracle.com/cd/E19059-01/stud.9/817-6698/Ch1.Intro.html#71468

One last note

It must be noted that Bjarne Stroustrup seems to be have been opposed to considering void foo(int) the same prototype as void foo(const int):

Not every feature accepted is in my opinion an improvement, though. For example, [...] the rule that void f(T) and void f(const T) denote the same function (proposed by Tom Plum for C compatibility reasons) [have] the dubious distinction of having been voted into C++ “over my dead body”.

Source: Bjarne Stroustrup
Evolving a language in and for the real world: C++ 1991-2006, 5. Language Features: 1991-1998, p21.
http://www.stroustrup.com/hopl-almost-final.pdf

This is amusing to consider Herb Sutter offers the opposite viewpoint:

Guideline: Avoid const pass-by-value parameters in function declarations. Still make the parameter const in the same function's definition if it won't be modified.

Source: Herb Sutter
Exceptional C++, Item 43: Const-Correctness, p177-178.


This has been discussed many times, and mostly people end up having to agree to disagree. Personally, I agree that it's pointless, and the standard implicitly agrees -- a top-level const (or volatile) qualifier doesn't form part of the function's signature. In my opinion, wanting to use a top-level qualifier like this indicates (strongly) that the person may pay lip-service to separating interface from implementation, but doesn't really understand the distinction.

One other minor detail: it does apply to references just as well as pointers though...


It makes the compiler do part of the work of catching your bugs. If you shouldn't be modifying it, make it const, and if you forget, the compiler will yell at you.


If bar is marked const as above, then the person reading the code, knowing what was passed in, knows at all time exactly what bar contains. There's no need to look at any code beforehand to see if bar got changed at any point along the way. This makes reasoning about the code simpler and thus reduces the opportunity for bugs to creep in.

I vote "good practice" myself. Of course I'm also pretty much a convert to functional languages these days so....


Addressing the comment below, consider this source file:

// test.c++

bool testSomething()
{
    return true;
}

int test1(int a)
{
    if (testSomething())
    {
        a += 5;
    }
    return a;
}

int test2(const int a)
{
    if (testSomething())
    {
        a += 5;
    }
    return a;
}

In test1 there is no way for me to know what the value being returned will be without reading the (potentially sizable and/or convoluted) body of the function and without tracking down the (potentially distant, sizable, convoluted and/or source-unavailable) body of the function testSomething. Further, the alteration of a may be the result of a horrific typo.

That same typo in test2 results in this at compile-time:

$ g++ test.c++
test.c++: In function ‘int test2(int)’:
test.c++:21: error: assignment of read-only parameter ‘a’

If it was a typo, it's been caught for me. If it isn't a typo, the following is a better choice of coding, IMO:

int test2(const int a)
{
    int b = a;
    if (testSomething())
    {
        b += 5;
    }
    return b;
}

Even a half-baked optimizer will generate identical code as in the test1 case, but you're signalling that care and attention will have to be paid.

Writing code for readability involves a whole lot more than just picking snazzy names.


I tend to be a bit of a const fiend so I personally like it. Mostly it's useful to point out to the reader of the code that the variable passed in wont be modified; in the same way that I try to mark every other variable that I create within a function body as const if it's not modified.

I also tend to keep the function signatures matching even though there's not much point in it. Partly it's because it doesn't do any harm and partly it's because Doxygen used to get a bit confused if the signatures were different.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号