?

Log in

 

Am I nuts? - Advanced C++ Community

About Am I nuts?

Previous Entry Am I nuts? Oct. 25th, 2006 @ 06:20 pm Next Entry
I'm rather offended, having been bitten by this bug. Why is it even allowed to take a reference to a const reference, if it could be a copy?!?

#include <assert.h>

class Blah
{
public:
  const unsigned int &myfoo;
  Blah(const unsigned int &p):
    myfoo(p) {}
  const unsigned int getfoo()
  {
    return myfoo;
  }
};

int main()
{
  int foo(42);
  Blah baz(foo);

  ++foo;

  assert(foo == baz.getfoo());

  return 0;
}
Current Mood: shockedshocked
Leave a comment
[User Picture Icon]
From:justben
Date:October 25th, 2006 04:32 pm (UTC)
(Link)
"const foo&" doesn't mean "a reference to a foo that cannot change". It means "a reference to a foo that *I* cannot change." Here, it's a guarantee to Blah's users that Blah won't change its uinsigned int.
[User Picture Icon]
From:xil
Date:October 25th, 2006 04:40 pm (UTC)
(Link)
That doesn't clarify why that assertion fails.
[User Picture Icon]
From:justben
Date:October 25th, 2006 05:03 pm (UTC)
(Link)
Yeah, I misread. My bad.
[User Picture Icon]
From:pphaneuf
Date:October 25th, 2006 04:49 pm (UTC)
(Link)
I know, that's what I wanted. What offends me is that the call to Blah's constructor makes a temporary unsigned int which is then the subject of this reference.

Taking out the const in the constructor parameters makes the compiler angry at the signedness without letting it make a temporary.

The part that strikes me as totally funky is why I'm allowed to keep a const reference without the merest warning, when it's so common that a temporary copy is involved...
[User Picture Icon]
From:justben
Date:October 25th, 2006 05:05 pm (UTC)
(Link)
Oh jeez, yeah, that caught me too: so much that I misread == as !=.
[User Picture Icon]
From:spasmaton
Date:October 25th, 2006 04:51 pm (UTC)
(Link)
I think his bug is more subtle than that, if you look closer...

Blah's ctor takes an unsigned int - as foo is an int the compiler will do an implicit conversion and pass the copy of the uint lvalue on the stack by reference to Blah(). The initialisation of Blah::myfoo then takes a reference to the uint on the stack, which not only isn't the variable he thinks it is but technically ceases to exist by the closing brace and only stays the same because nothing else happens with the memory in the meantime.

What's really happening is:
int foo(42);
unsigned int compiler_temp(foo);
Blah baz( compiler_temp );

Where compiler_temp is implicitly being created and cleared up.

I think it's better to get your head around constness and implicit conversions, and avoid references-as-member-variables and pass instances as pointers for clarity (the compiler would have moaned if the ctor had been a pointer rather than a reference).
[User Picture Icon]
From:pphaneuf
Date:October 25th, 2006 04:55 pm (UTC)
(Link)
I know, it's totally the reference to the temporary that kills it. I'm appalled at the complete lack of warnings.

The const reference gives the compiler a complete license to build temporaries. Removing the const makes the compiler error out, and using a const pointer works.

I prefer references normally, as it's non-trivial to get a null reference in them, but this isn't good for my blood pressure...
[User Picture Icon]
From:spasmaton
Date:October 25th, 2006 05:13 pm (UTC)
(Link)
I prefer references normally, as it's non-trivial to get a null reference in them

It should be, but it isn't. It just takes one sloppy use of a pointer dereference to create one, and if you're persisting them as member variables and always making assumptions about them being valid you'll fall over them long after the mistake has happened and spend ages scratching your head over them.

I wouldn't be 'appalled' by this, the compiler can only do so much and a reference member variable is a bit seat-of-the-pants anyway. I'd go for a pointer for clarity, both in the call and the member, infact that's been coding standards at a couple of places I've worked.
(Deleted comment)
[User Picture Icon]
From:omnifarious
Date:October 25th, 2006 06:09 pm (UTC)
(Link)

I use reference members sometimes. I use it as a 'hard const' pointer. I'm hoping the compiler will make optimization assumptions based on the fact that the reference can't change what it's referencing for its entire lifetime.

But, I always use them to reference things that were either passed in as pointers, or things I control the lifetime of myself, like pimpls.

(Deleted comment)
[User Picture Icon]
From:omnifarious' OpenID account [omnifarious.org]
Date:October 25th, 2006 08:39 pm (UTC)
(Link)

Well, perhaps someday compilers will be good enough to recognize that a particular smart pointer is really immutable and do the right thing. :-) Otherwise such a beast is likely even less efficient than X * const ptr. Of course, designing things to make the compiler generate fast code is a dubious prospect in general, so I probably should stop just on those grounds.

But, you do bring up a good point, and if I'm not mistaken, the vast majority of the places where I use a reference member variable use a non-const reference.

Though it's possible I sometimes have code that looks like this:

class Y;

class A {
 public:
   A(const Y *yp) : y(*yp) { if (! yp) { throw NullPointerException; } }
 private:
   const Y &y;
};
[User Picture Icon]
From:xil
Date:October 26th, 2006 08:04 pm (UTC)
(Link)
I'd imagine most smart pointers would be optimized to be extremely close to X * const ptr, if not the same. Assuming your smart pointer doesn't have error checking (checking for null on dereference), which would be kind of a stupid design except for debugging anyway, most operators and function calls will be inlined. Assuming the smart pointer is const, the internal pointer will be const, and the compiler should be able to perform the same optimizations after inlining and template resolution.
[User Picture Icon]
From:spasmaton
Date:October 25th, 2006 08:58 pm (UTC)
(Link)
There's a whole load of voodoo involved with how compilers optimise, but as there's no true const in C++ it very rarely can make assumptions about anything. A much clearer way to make sure an address isn't reloaded is make the aliasing clear - i.e. take a local reference to it as soon as its needed in a function (though what's being pointed to may still be aliased). But that's a compiler dependant dance within itself.

If you want a 'hard const' pointer then an actual *const is an option. Then I'd have said a pimpl idiom is pretty clear anyway.
[User Picture Icon]
From:cbradfield
Date:October 25th, 2006 05:31 pm (UTC)
(Link)
Why aren't there any warnings? Because it is completely legal to take a const& of a temporary AND use it! Andrei Alexandrescu uses this language feature in his ScopeGuard class. To quote the article:
Now we'll disclose the whole mechanism. According to the C++ Standard, a reference initialized with a temporary value makes that temporary value live for the lifetime of the reference itself.
This feature is useful as it allows you to take advantage of polymorphism without allocating a named instance or allocating an instance of the derived object dynamically.
[User Picture Icon]
From:omnifarious' OpenID account [omnifarious.org]
Date:October 25th, 2006 06:04 pm (UTC)
(Link)

I consider this (even the lack of warnings) to be completely reasonable. Frequently a temporary is created and passed in as a const reference. The compiler does, in fact, issue a warning if the temporary is passed in as a non-const reference, which is also the right thing to do.

The line the compiler _should_ issue a big fat warning for is the member variable initializer myfoo(p). One strong guideline for references is to avoid persisting them.

With pointers a specific pointer type has to be passed in. And this clue in the person who's passing it that maybe they should be concerned about the lifetime of the information being pointed to.

For references there is no such clue. And functions that are passed references should always assume that the referenced thing will go away as soon as control leaves the function.

So the error here is where the reference is persisted, which the compiler can detect in this case, and it should issue a strong warning about it.

[User Picture Icon]
From:spasmaton
Date:October 25th, 2006 09:17 pm (UTC)
(Link)
It's trickier than you'd think on first glance. The problem is spread between two points - it's the line Blah baz(foo); where the temporary is being created, to match the function spec for the ctor, then its reference is being persisted within the code body of the constructor. It's unreasonable to expect a compiler to warn about an issue spread out like that, something like lint may pick it up but compilers can only spend so long looking at whacky-but-legitimate things the user is telling them to do.
[User Picture Icon]
From:omnifarious' OpenID account [omnifarious.org]
Date:October 25th, 2006 10:54 pm (UTC)
(Link)

I disagree. The case of "Initialize a reference member variable with a reference constructor argument" is possible to detect without any deep analysis that crosses function boundaries and is also almost always wrong.

And any lint tool worth it's salt should definitely pick this up when doing analysis across function boundaries.

(Deleted comment)
[User Picture Icon]
From:omnifarious
Date:October 26th, 2006 12:49 pm (UTC)
(Link)

You can initialize it with...

  • A dereferenced pointer that's been passed in
  • A reference returned by a static member function
  • A dreferenced pointer from new (the pimpl idiom)
  • A dereferenced pointer returned by a static member function

In short, any number of places. A reference constructor argument comes in from the outside world and you have no idea where it came from. The person calling your constructor also may have no idea they're passing a reference and not a value. So squirreling away the reference is likely to result in surprising behavior in many instances.

(Deleted comment)
[User Picture Icon]
From:omnifarious
Date:October 25th, 2006 10:58 pm (UTC)

Not as hard as all that

(Link)

I don't think it's as hard as you say. See this comment.

[User Picture Icon]
From:coises
Date:October 27th, 2006 06:27 am (UTC)
(Link)

One of the frustrating things about C++ is that because it can express so many things about proper use of objects and functions, one is led to expect to be able to express anything that’s logically consistent.

I think want you wanted to express was:

  • A Blah can be constructed using a reference to an unsigned int.
  • Blah won’t modify the referenced variable, so it can be either const or non-const.
  • A temporary should never be created (or, perhaps, should never be created from an argument which is not inherently const).

I can think of no way to express this in C++; you cannot allow passing of a reference to const without allowing the creation of a temporary, and you can’t impose restrictions on the creation of the temporary beyond those implied by the rules of conversion.

I can think of two work-arounds, both of which interfere with the desired syntax of the call:

  1. Change the syntax of the constructor to require passing a pointer to const instead of a reference. An unsigned int* will convert to a const unsigned int*, but an int* won’t. Unfortunately, in addition to requiring the use of a semantically illogical (from the point of view of the caller) & in front of the variable in every call, the possibility of a null pointer must be handled as a run-time check.
  2. Make the constructor argument non-const. This will require the use of const_cast<unsigned int&> in the (presumably rare) case in which the argument is a const unsigned int, but it will prevent the unintentional creation of a temporary.

There might be some sense to a compiler issuing a warning about using a const reference parameter to initialize a reference member... but then, there are so many illogical things you can do with pointers and references, where would it end? My compiler (VC++6) doesn’t even warn about this:

class Blah {
 public:
   const unsigned int &myfoo;
   const unsigned int* myfoo2;
   Blah(unsigned int p): myfoo(p), myfoo2(&p) {}
   };

which is totally irrational (note that the argument to the constructor is not a reference).

[User Picture Icon]
From:migashko
Date:January 27th, 2007 05:12 pm (UTC)

simple example

(Link)
(Leave a comment)
Top of Page Powered by LiveJournal.com