Domanda

There are some things I am pretty strict about, but const has not been one of them.

For example, I might make a local variable that I use three times in a function, that does not get changed, and yet I do not bother making it const.

Is this something that I should be more strict about? Are there any tangible benefits that I should be aware of? Is this something you would flag if you were reviewing my code?

È stato utile?

Soluzione

Is this something that I should be more strict about? Are there any tangible benefits that I should be aware of?

As @JerryHeremiah points out, after some point it doesn't add anything for compiler optimization, but it can improve readability.

I tend to do this (use const when possible, for extra readability), because it fits the Law of Demeter ("if this value doesn't need to be modified, then do not allow it to be, past this point") and expresses the author's intent better (in a way the compiler checks for).

Is this something you would flag if you were reviewing my code?

Depends on the agreed-upon review code criteria for a project (yes, it is something I would flag, if this were a condition to be flagged for the coding standards of the project, and I would also argue that this should be a condition to be flagged for a project).

Altri suggerimenti

Make interfaces easy to use correctly and hard to use incorrectly

const allows you to do just that. If your function returns a set of items that the caller of the function isn't allowed to modify, don't rely on the caller reading the documentation, just return const items, resp const_iterator. Not only is it less likely that the caller will make a mistake, using const is also far less typing than writing a comment that says the caller shouldn't modify the items.

The "issue" with const is, that it propagates. If your code isn't const correct, you're making it much harder for the code that's using your code to be const correct. So if you don't write const correct public interfaces, you force people to either use brittle hacks, or to join you in not writing const correct code, which results in many violations of that quote at the top of my answer.

Outside the publicly accessible interfaces, there are indeed diminishing returns, because you don't have to worry as much about const propagation. However, in these cases consider const to be a form of documentation - one which is effortless to read and write. If the code is sufficiently trivial, that documentation is simply not needed, but in most if not all cases it's far easier to write const than it is to figure out if the code is trivial enough to omit it.

I would say any local variable that doesn't change should be constexpr if possible and const reference if not. If you're going to modify a variable you really need to know if it's a reference to or a local copy. If you get it wrong your program's not going to do what you expect it to do. However if your variable is const it doesn't matter whether it's a reference or a copy. The language supports this by allowing a const ref to bind to a temporary.

// some functions
int& by_ref();
int by_val();

int main(){
  {
    auto a = by_ref();  // takes a copy
    const auto b = by_ref();  // also takes a copy
    auto& c = by_ref();  // no copy
    const auto& d = by_ref();  // no copy 
  }
  {
    auto a = by_val();  // might copy
    const auto b = by_val();  // might copy
    auto& c = by_val();  // error reference to temporary
    const auto& d = by_val();  // allowed! no copy.
  }
}

Notice that a, b and c all behave differently depending on whether the assignment is by reference or by value. But d is the same on both cases.
If a function returns a reference then d will just refer to that value, if it returns a value, then d will refer to a temporary object the lifetime of that object will be extended until d goes out of scope.

So by using const auto& wherever possible your code is more robust to changes in the rest of the code. You can change the return value from reference to value without breaking the code and from value to reference without incurring an extra copy.

If you construct an instance of a class in your function then using a const reference is a bit strange and might confuse someone reading it. For a built in type is has already been pointed out that there's probably no performance benefit to marking it const but for a more complex type there are good reasons to do so. Consider the following.

class my_class{

protected:
    struct my_data {
      int x, y, z;
    };
    ~my_class() = default;

private:
    virtual my_data& data() = 0;
    virtual const my_data& data() const = 0;

public:
    auto& x() {
      return data().x;
    }
    auto& y() {
      return data().y;
    }
    auto& z() {
      return data().z;
    }

    auto& x() const{
      return data().x;
    }
    auto& y() const{
      return data().y;
    }
    auto& z() const{
      return data().z;
    }
};

You'll notice that I've defined everything in my_class twice. I'm a lazy guy so I'm not going to do that without a good reason. The reason is that a const instance of my_class can only access the methods labelled const. So I need to provide both const and non const versions of some functions and my natural instinct is to wonder whether I really need to do all that typing.

It's pretty obvious for this version that we need the const versions of all the functions. It's clearly an abstraction and I can't know how it's going to be implemented or used. It's reasonable to assume that someone will want a const my_class somewhere and they should still be allowed to use all the methods.

However as I normally prefer to abstract through templates I've sometimes got a little class that I'm only going to use locally or as a workaround for some weird bit of syntax so it can be really tempting to provide only the non const version of a function and only use non const instances.

Which works great until it stops working. When at some point somewhere a const version of something instantiates a const version of my class we get one of those war and peace style error messages that template meta programmers love so much.

So the moral is always define const and non const versions of member functions. The reasons are exactly the same as for the my_class example but there not always entirely obvious in every context. Unfortunately if it's not obvious why you should do something then it's usually not obvious that not doing it is what caused an error or a bug.

So if you always declare variables as const unless you need to alter them you'll catch any class that doesn't have const versions of its functions. This means you'll be less reluctant to put them in in the first place because you know you're going to catch the error early enough that you have to fix it yourself.

Even if you didn't define the class yourself there might be subtly different versions of some methods for const and non const.

It shouldn't usually be the case that const and non const member functions of a class are dramatically different but there's nothing in the language that says they have to be the same. So by declaring a local variable const you could have slightly different behaviour and it's probably the const behaviour you want. The only example I can think of where it might incur a performance hit is in a multithreaded situation. Accessing a non const member function might involve taking a write-lock which would have to wait for all readers to exit, but the const version might only involve waiting for one writer to finish.

So there are good reasons to prefer const in many situations although for built in types and some small classes the arguments aren't quite as strong. I would recommend in this case to apply the unthinking rule always prefer const than to make a few exceptions with no great benefit.

For local primitive variables, I think constness is very useful as a short-hand when tracing through a function. It immediately communicates to me, "this never changes", and if you're trying to debug an issue associated with state changes, everything defined as a constant lets you quickly omit that from your list of suspects. I never found it quite as valuable for defensive programming as it is in communicating programmer intentions succinctly.

When we're debugging code, we're usually spending the bulk of our time examining states and how they change over the course of each line of code. const immediately says, "You don't have to keep a watch over me, I'm not going to change" and so I do find that incredibly valuable and recommend you start using const more often for locals which don't need to change.

Where I think C and C++ messed up was how constness interacts with user-defined types. If you have something like this:

// Does this cause side effects?
void do_something(const iterator first, const iterator last);

... does the algorithm modify the range or not? That's not a question that's answered by constness here, because const only modifies iterator in preventing it from modifying the pointer, not the pointee. It's also easy enough to copy first and last to copies where both pointer and pointee can be modified. As a result, the nature of the language demands a whole separate const_iterator type to prevent the pointee from being modified:

// This probably doesn't cause side effects.
void do_something(const_iterator first, const_iterator last);

Same case with:

// Some kind of handle.
class Foo
{
public:
    ...

    // Does this cause side effects?
    void some_func() const;

private:
    Bar* ptr;
};

Same scenario as const iterator. Since Foo stores a pointer to Bar, marking some_func as read-only doesn't prevent it from modifying the contents of Bar, the pointee. It just prevents it from modifying the pointer.

The idea that const applies to pointers rather than pointees for UDT members seems incredibly short-sighted, because typically if there are side effects we should be concerned about, it's with respect to the pointee, not the pointer. A function that accepts const iterator can't cause side effects through that parameter no matter what it does to the pointer as long as it doesn't touch the pointee, yet the constness applies the other way around.

The real question of interest when functions are involved is typically whether or not they cause side effects, and const was never sufficient in these two languages to even express and communicate a clear indication of that. I wish this problem could be addressed, perhaps with a new keyword that prevents functions from invoking any side effects whatsoever to variables outside their own scope (not even being able to call other functions which cause side effects and not being able to define function-local static variables). That would be one of my dream safety features. Until then, I've stopped bothering with const-correctness so much at the UDT level to the point where I'm trying to make two different classes (one read-only class, one mutable class) for everything, because I find that generally costs more time than it saves.

Is this something that I should be more strict about? Are there any tangible benefits that I should be aware of? Is this something you would flag if you were reviewing my code?

No. On the contrary, const is by and large a complete waste of time that results in nothing more than obsessively marking everything as const despite the fact that it's clearly non-mutating. Using const where you don't need to will just result in endless pain making everything const and duplicating every overload and such nonsense.

const is justified in e.g. set keys, but beyond that, it's a complete waste of time.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top