tags:

views:

395

answers:

6

I'm adding some lazy initialization logic to a const method, which makes the method in fact not const. Is there a way for me to do this without having to remove the "const" from the public interface?

int MyClass::GetSomeInt() const
{
    // lazy logic
    if (m_bFirstTime)
    {
        m_bFirstTime = false;
        Do something once
    }

    return some int...

}

EDIT: Does the "mutable" keyword play a role here?

+1  A: 

set the m_bFirstTime member to be mutable

e.James
+10  A: 

Make m_bFirstTime mutable:

class MyClass
{
  : :
  mutable bool m_bFirstTime;
};

...but this is also very often an indication of a design flaw. So beware.

John Dibling
I think it is ok for things like internal caching, which don't really change the state of the object (IE are somewhat redundant).
ypnos
That's EXACTLY what I'm using it for
Corey Trager
+6  A: 

Actually, you said that you didn't want to change the header file. So your only option is to cast away the constness of the this pointer...

int MyClass::GetSomeInt() const
{
    MyClass* that = const_cast<MyClass*>(this);

    // lazy logic
    if (that->m_bFirstTime)
    {
        that->m_bFirstTime = false;
        Do something once
    }

    return some int...

}

If using mutable raises a red flag, this launches a red flag store in to orbit. Doing stuff like this is usually a really bad idea.

John Dibling
I'm pretty sure this is going to jump into undefined behavior in at least some cases. As I recall, it is only legal to cast away constness if the value was originally non-const. If it was initially declared const, you may not cast away the constness. (IIRC. Don't have the standard here to check)
jalf
I think it is legal here as the original class was not const, only the function's access to it.
ypnos
But what if someone has declared a global const object? OK, so the chances of any compiler making its non-mutable fields physically read-only once initialization is over, is pretty small. But AFAIK it's allowed to.
Steve Jessop
A: 

In any case - make note that this is no longer going to be thread safe. You can often rely on an object to be thread safe if it only has const methods (or you use only the const methods after initialization). But if those const methods are only logically const, then you lose that benefit (unless of course you start locking it all the time).

The only compiler optimization that could cause havok would be for the compiler to figure out that you're calling the method twice with the same arguments, and just reuse the first return value - which is fine as long as the function truly is logically const, and returns the same value for a given set of arguments (and object state). Even that optimization isn't valid if it's possible that anyone (including another thread) has had access to the object to call non-const methods on it.

mutable was added to the language specifically for this case. C++ is a pragmatic language, and is happy to allow corner cases like this to exist for when they are needed.

Eclipse
"anyone (including another thread) has had access". Or even the same thread via another reference, which is why the compiler often can't make any optimizations just because something is const. Compilers can ignore the effects of other threads, usually provided there hasn't been a memory barrier.
Steve Jessop
... but of course for C++98 all that stuff is implementation-dependent anyway, so they make their own rules about whether and in what order changes to memory made in one thread are seen in another thread.
Steve Jessop
+5  A: 

I think of this problem as involving two concepts: (1) "logically const" and (2) "bitwise const". By this I mean that getting some int from a class, does not logically change the class and in most cases it does not change the bits of the class members. However, in some cases, like yours, it does.

In these cases, where the method is logically const but not bitwise const, the compiler cannot know this. This is the reason for the existence of the mutable keyword. Use it as John Dibling shows, but it is not a design flaw. On the contrary, there are many cases where this is necessary. In your example, I presume that the calculation of the int is expensive, so we do not want to calculate it if it is not needed. In other cases, you may wish to cache results of methods for later use, etc.

BTW, even though you have accepted the "mutable" answer as correct, you do have to update the .h!

Andrew Stein
+1  A: 

As John Dibling said, mark the fields that are changed as mutable. The important part is in the comment by ypnos: 'don't really change the state of the object' (as perceived by the outside world). That is, any method call before and after the const method call must yield the same results. Else your design is flawed.

Some things that make sense to be mutable:

  • mutex or other lock types
  • cached results (that will not change)

Mutex are not part of your objects state, they are only a blocking mechanism to guarantee data integrity. A method that will retrieve a value from your class, does need to change the mutex, but your class data and state will be exactly the same after the execution of the const method as it was before.

With caching, you must consider that only data that it makes sense for data that is expensive to retrieve and assumed not to change (DNS result, as an example). Else you could be returning stale data to your user.

Some things that should not be changed inside const methods:

  • Anything that modifies the state of the object
  • Anything that affects this or other method results

Any user of your class that executes const methods will assume that your class (as seen from the outside world) will not change during the execution. It will be quite misleading and error prone if it were not the same. As an example, assume that a dump() method changes some internal variable -state, value- and that during debug the user of your class decides to dump() your object at a given point: your class will behave differently with traces than without: perfect debug nightmare.

Note that if you do lazy optimizations you must do them to access immutable data. That is, if your interface dictates that during construction you will retrieve an element from the database that can be later accessed through a constant method, then if you do lazy fetching of the data you can end up in a situation where the user constructs your object to keep a copy of the old data, modifies the database and later on decides to restore the previous data into the database. If you have performed a lazy fetch you will end up loosing the original value. An opposite example would be configuration file parsing if the config file is not allowed to be modified during the execution of the program. You can avoid parsing the file up the point where it is needed knowing that performing the read in the beginning or at a later time will yield the same result.

David Rodríguez - dribeas