13

The following code is, as far as I understand it, undefined behavior according to the c++ standard (section 7.1.5.1.4 [dcl.type.cv]/4 in particular).

#include <iostream>

struct F;
F* g;

struct F {
    F() : val(5)
    {
        g = this;
    }
    int val;
};


const F f;

int main() {
    g->val = 8;
    std::cout << f.val << std::endl;
}

However, this prints '8' with every compiler and optimization setting I have tried.

Question: Is there an example that will exhibit unexpected results with this type of "implicit const_cast"?

I am hoping for something as spectacular as the results of

#include <iostream>
int main() {
    for (int i = 0; i <=4; ++i)
        std::cout << i * 1000000000 << std::endl;
}

on, e.g., gcc 4.8.5 with -O2

EDIT: the relevant section from the standard

7.1.5.1.4: Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.

In reply to the comment suggesting a duplicate; it is not a duplicate because I am asking for an example where "unexpected" results occur.

JRG
  • 2,065
  • 2
  • 14
  • 29
  • 2
    That's a very devious setup - very nice. – Kerrek SB Jun 08 '16 at 16:59
  • 7
    I do like the symptoms of that snippet at the bottom. Zero, one, two, release the Kraken ! – Quentin Jun 08 '16 at 16:59
  • 3
    What is the value in trying to define undefined behavior? – lcs Jun 08 '16 at 16:59
  • Can you quote `7.1.5.1.4`? I do not have that in my standard. – NathanOliver Jun 08 '16 at 17:00
  • 1
    Possible duplicate of [Why does C++ not have a const constructor?](http://stackoverflow.com/questions/6936124/why-does-c-not-have-a-const-constructor) – Jean-Baptiste Yunès Jun 08 '16 at 17:04
  • @NathanOliver [dcl.type.cv]/4 – T.C. Jun 08 '16 at 17:04
  • @T.C. Thanks for that – NathanOliver Jun 08 '16 at 17:05
  • What would you _expect_ this code to do? – Oktalist Jun 08 '16 at 17:09
  • A system could mark the memory containing `const F f;` as read-only, in which case you'd get a segfault. I'm not aware of any system that does such a thing though. – Mooing Duck Jun 08 '16 at 17:30
  • Undefined behavior means that you just can't expect something to happen. It may print 8 on your computer and crash on another one. Nothing strange is happening with your example. – Alexander Dyagilev Jun 08 '16 at 17:31
  • 1
    @Kaz Unfortunately, "fixes" that break previously valid code is against the principles of C++. And even then, even by today's standards, the fact that C++ allows "hacks" of this sort is one of the selling features of the language, as it allows some people to ship their projects on time no matter the issues you get at the last minute, even at the cost of maintenability. Nice off-topic rant, though – KABoissonneault Jun 08 '16 at 17:58
  • @KABoissonneault There is no justification for not diagnosing this type violation. The assignment to `g`, if it were diagnosed, *could* be happily forced into working with a `static_cast`. As far as old code goes, that's what dialect-selection compiler switches are for. If you want to write in C++98 for as long as you live, just use the compiler's switch for C++98 for as long as you live. Leave the backward compatibility for people who like computers to have a `C:` drive. Anyway C++ has made incompatible steps which break code, like turning string literals `const`, which was laudable. – Kaz Jun 08 '16 at 18:07
  • 1
    @Kaz There are plenty of things that cannot easily be done in the member initializer list, which under your proposal would require lots of helper functions (or worse, `mutable` sprinkled everywhere). – T.C. Jun 08 '16 at 18:45
  • @T.C. Yes, and so maybe objects that require complex, multi-step initialization shouldn't be defined as `const`! Instantiate them non-const and bind them to const references or pointers. Also, there could be a mechanism of declaring the entire constructor `mutable` with that very keyword. Then, in spite of the object being `const`, inside the body of that constructor, those old-fashioned, backward-compatible shenanigans are allowed (assigning to members, and `this` being an unqualified pointer) without a diagnostic. Without the `mutable`, it's strict base/member inits. – Kaz Jun 08 '16 at 21:02
  • @Kaz I suppose you can come up with a way to make it work, but frankly I don't see the benefit as being worth all this extra complexity. – T.C. Jun 08 '16 at 21:06
  • 1
    I'm naive here but isn't this classic example of aliasing problem? – Mukul Gupta Jun 08 '16 at 21:09

2 Answers2

7

Not as spectacular:

f.h (guards omitted):

struct F;
extern F* g;

struct F {
    F() : val(5)
    {
        g = this;
    }
    int val;
};

extern const F f;
void h();

TU1:

#include "f.h"
// definitions
F* g;
const F f;
void h() {}    

TU2:

#include "f.h"
#include <iostream>
int main() {
    h(); // ensure that globals have been initialized
    int val = f.val;
    g->val = 8;
    std::cout << (f.val == val) << '\n';
}

Prints 1 when compiled with g++ -O2, and 0 when compiled with -O0.

T.C.
  • 133,968
  • 17
  • 288
  • 421
0

The main case of "undefined" behavior would be that typically, if someone sees const, they will assume that it does not change. So, const_cast intentionally does something that many libraries and programs would either not expect to be done or consider as explicit undefined behavior. It's important to remember that not all undefined behavior comes from the standard alone, even if that is the typical usage of the term.

That said, I was able to locate a place in the standard library where such thinking can be applied to do something I believe would more narrowly be considered undefined behavior: generating an std::map with "duplicate keys":

#include "iostream"
#include "map"

int main( )
{
    std::map< int, int > aMap;

    aMap[ 10 ] = 1;
    aMap[ 20 ] = 2;

    *const_cast< int* >( &aMap.find( 10 )->first ) = 20;

    std::cout << "Iteration:" << std::endl;
    for( std::map< int,int >::iterator i = aMap.begin(); i != aMap.end(); ++i )
        std::cout << i->first << " : " << i->second << std::endl;

    std::cout << std::endl << "Subscript Access:" << std::endl;
    std::cout << "aMap[ 10 ]" << " : " << aMap[ 10 ] << std::endl;
    std::cout << "aMap[ 20 ]" << " : " << aMap[ 20 ] << std::endl;

    std::cout << std::endl << "Iteration:" << std::endl;
    for( std::map< int,int >::iterator i = aMap.begin(); i != aMap.end(); ++i )
        std::cout << i->first << " : " << i->second << std::endl;
}

The output is:

Iteration:
20 : 1
20 : 2

Subscript Access:
aMap[ 10 ] : 0
aMap[ 20 ] : 1

Iteration:
10 : 0
20 : 1
20 : 2

Built with g++.exe (Rev5, Built by MSYS2 project) 5.3.0.

Obviously, there is a mismatch between the access keys and the key values in the stored pairs. It also seems that the 20:2 pair is not accessible except via iteration.

My guess is that this is happening because map is implemented as a tree. Changing the value leaves it where it initially was (where 10 would go), so it does not overwrite the other 20 key. At the same time, adding an actual 10 does not overwrite the old 10 because on checking the key value, it's not actually the same

I do not have a standard to look at right now, but I would expect this violates the definition of map on a few levels.

It might also lead to worse behavior, but with my compiler/OS combo I was unable to get it to do anything more extreme, like crash.

  • @T.C. Can't really give citations for undefined behavior, *but* I was wrong about that point. I investigated in some more depth and added that as an edit. The real situation is stranger. –  Jun 08 '16 at 18:59
  • What is *soft* undefined behavior? – nwp Jun 08 '16 at 20:44
  • If you mess with the internals of a class in a way that violates its contract and upsets its invariants, of course it's going to break down, whether or not `const_cast` is used in the process. – T.C. Jun 08 '16 at 20:46
  • @T.C. Exactly the point, except that doing this with standard-defined functionality becomes undefined behavior. –  Jun 08 '16 at 20:48
  • @nwp I explained it in the second half of that sentence. It lets you easily break things which assume you are going to obey normal conventions. It isn't quite *undefined behavior* in the sense the standard says it is undefined / doesn't define it, but it almost always _is_ in the sense individual programs/libraries would be extremely easily broken by using it. But that said, when you do it on the standard, in my opinion that becomes entirely "undefined behavior". You are doing things that the standard has not defined with what it has provided. And obviously, it isn't properly handled. –  Jun 08 '16 at 20:49
  • I adjusted the wording on that sentence to get my point across more clearly. –  Jun 08 '16 at 21:22