0

Here is a reduced code snippet I needed to debug today.

I accidently typed something like this:

QImage myImage = LoadImage(path);
QImage scaledImage = myImage.scaled(100, 100);
if (condition)
{
   QImage scaledImage = scaledImage.mirrored(true, true); // *** Crash ***
}

Instead of the intendend code:

QImage myImage = LoadImage(path);
QImage scaledImage = myImage.scaled(100, 100);
if (condition)
{
   scaledImage = scaledImage.mirrored(true, true); // *** Works fine ***
}

I would have expected the flawed code to either work as expected or to generate a compiler error. But it simply crashed with a division by zero. Why? Can someone tell me the compiler's point of view about this mistake?

Silicomancer
  • 8,604
  • 10
  • 63
  • 130
  • Which line of code failed? If innards of the `if` condition I'm guessing. There's no reason why the compiler would issue a warning or error since it's totally valid code. The only thing that I can imagine is wrong is the `QImage` in the `if` statement deletes the resources which causes the instance of `QImage` in the next scope up to do the same. Doesn't jive with the error you're getting but anything could end up causing it since you would likely end up with undefined behavior depending on how well `QImage` handles it's resources. – Captain Obvlious Nov 01 '14 at 23:19
  • I already marked the line with a comment "Crash" (easy to miss I guess). – Silicomancer Nov 01 '14 at 23:19

2 Answers2

3

Think about what this line of code does:

QImage scaledImage = scaledImage.mirrored(true, true); // Crash
  1. The symbol scaledImage is defined. This scaledImage symbol name overrides the one of the same name in the outer scope.
  2. Calls the mirrored() method.
  3. scaledImage is now created using the copy constructor with the output of mirrored().

As PeterT points out in the comments, this is undefined behavior: you're calling a method on an object before it's created. In this case crashing helped you avoid a mistake that's easy to make.

Here's an example that demonstrates exactly how and why this problem exists:

class Tester {
public:
    Tester() {
        qDebug() << "Default c'tor";
    }

    Tester( const Tester& other ) {
        qDebug() << "Copy c'tor";
    }

    Tester& Tester::operator=( const Tester& other ) {
        qDebug() << "Assignment";
        return *this;
    }

    Tester& test() {
        data = "test";
        return *this;
    }
};

int main(int argc, char *argv[])
{
    Q_UNUSED(argc);
    Q_UNUSED(argv);

    Tester test = test.test();
    return 0;
}

The program will output the following:

Test

Copy c'tor

In other words, the test() method was called before any constructor was called. That's bad!

But it gets much worse if we modify our example with a data member:

class Tester {
    QString data;
public:
    Tester() {
        qDebug() << "Default c'tor";
        data = "data";
    }

    Tester( const Tester& other ) {
        qDebug() << "Copy c'tor";
        data = other.data;
    }

    Tester& Tester::operator=( const Tester& other ) {
        qDebug() << "Assignment";
        data = other.data;
        return *this;
    }

    Tester& test() {
        data = "test";
        qDebug() << "Test";
        return *this;
    }

};

Now the program crashes before anything can be printed out. Specifically, the first line in test() is the culprit:

data = "test";

If you think about it, this is trying to assign something to a QString that hasn't yet been constructed. Any attempt to access or modify the member variables of an unconstructed object is bad news.

MrEricSir
  • 8,044
  • 4
  • 30
  • 35
  • @PeterT Good point, I'd originally gotten the order of operations wrong. I've updated my answer with some interesting test cases that illustrate the issue. Thanks! – MrEricSir Nov 02 '14 at 00:04
  • 1
    This will not even give you a warning, not even with `-Wall -pedantic` isn't C++ grand :P ? – PeterT Nov 02 '14 at 00:08
  • Yes. It is amazing that this isn't fixed in th C++ standard up to 2014. It is such an nasty behaviour. – Silicomancer Nov 02 '14 at 09:06
1

The reason is still there if you reduce this code.

This:

QImage myImage = LoadImage(path);
QImage scaledImage = myImage.scaled(100, 100);
if (condition)
{
   QImage scaledImage = scaledImage.mirrored(true, true); // *** Crash ***
}

has the same error as this:

QImage myImage = LoadImage(path);
QImage scaledImage = scaledImage.mirrored(true, true); // *** Crash ***

Because the scaledImage on the right side is calling a function on the uninitialized object from the left side. This is undefined behavior.

PeterT
  • 7,981
  • 1
  • 26
  • 34
  • So the C++ standarad does not cover that case as an error? That's horrible, why isn't this considered an error? – Silicomancer Nov 01 '14 at 23:34
  • I don't know exactly where this was covered in the standard but this is mentioned to be undefined behavior. – PeterT Nov 01 '14 at 23:42
  • 1
    @Silicomancer this question will help you with the details: http://stackoverflow.com/questions/9820027/using-newly-declared-variable-in-initialization-int-x-x1 – PeterT Nov 01 '14 at 23:51
  • Looks like it might be because QImage isn't handling self assignment? – paulm Nov 01 '14 at 23:52
  • @paulm no, this is not on QImage, it's undefined behavior if you do `int x= x;` too – PeterT Nov 01 '14 at 23:52
  • but thats because x isn't initialized, here QImage is because its constructor is called? – paulm Nov 01 '14 at 23:53
  • 1
    @paulm no, it's not called, the space is allocated but no constructor is yet called. For most purposes `string x = "abc";` is the same as `string x("abc");`, not the same as `string x; x="abc";` – PeterT Nov 01 '14 at 23:53