0

Why does this code work correctly?

struct A {
    std::string value = "test"s;
    
    A() { std::cout << "created" << std::endl; }
    ~A() { std::cout << "destroyed" << std::endl; }
};

int main() { 
    A* ptr = nullptr;
    
    {
        A a;
        ptr = &a;
    }
    
    std::cout << ptr->value << endl; // "test"
}

output:

  • created
  • destroyed
  • test

Or this example:

struct A {
    const std::string* str = nullptr;
    
    A(const std::string& s) : str(&s) {}
};

int main()
{
    A a = std::string("hello world");
    std::cout << *a.str;

    return 0;
}

output:

  • hello world

In the first case, it seemed to me that when object A was destroyed, the content would become invalid, but I was able to retrieve the string.

In the second case, I took rvalue by constant reference, but extending the life of the object should work as long as the reference is valid. I thought that after the constructor worked, the string should have been destroyed. Why is this not happening?

mashau
  • 7
  • 2
  • 1
    What makes you think that "being invalid" = "It's easily observable as wrong"? – StoryTeller - Unslander Monica Apr 13 '22 at 09:14
  • 1
    what did you expect when you printed the contents of an already deleted string? – 463035818_is_not_an_ai Apr 13 '22 at 09:15
  • The program has undefined behavior. – Jason Apr 13 '22 at 09:17
  • 1
    *"Why does this code work correctly"* - it doesn't; you just think it does because you are incorrectly assuming *observed* behavior is *defined* behavior. The latter leads to the former; not the other way around. – WhozCraig Apr 13 '22 at 09:17
  • So in all these examples there will be undefined behavior? – mashau Apr 13 '22 at 09:17
  • If by "all" you mean "both" (as in, the two shown here) then yes. – WhozCraig Apr 13 '22 at 09:18
  • Yes. Specifically _In the second case, I took rvalue by constant reference, but extending the life of the object should work as long as the reference is valid._ The life-time of that const reference ends with the semicolon of `A a = std::string("hello world");`. Thus, the `a.str` is a dangling pointer in the `std::cout << *a.str;`. You can try this in VisualStudio with Debug mode. In Debug mode, the compiler adds code to wipe out destroyed instances with test patterns. This is quite enlightening sometimes (and quite annoying in some edge cases). – Scheff's Cat Apr 13 '22 at 09:19
  • 1
    See [Eric Lippert’s explanation](https://stackoverflow.com/a/6445794/1968). – Konrad Rudolph Apr 13 '22 at 09:28

2 Answers2

1

Both codes have undefined behavior.

Here:

{
    A a;
    ptr = &a;
}

std::cout << ptr->value << endl; // "test"

ptr becomes invalid once a goes out of scope and gets destroyed.

Similar in the second example, you are also dereferencing an invalid pointer because the temporary string is gone after the call to the constructor.

C++ does not define what happes when you do things that are not defined (makes sense, no? ;). Instead it is rather explicit about saying that when you do certain wrong things then the behavior of the code is undefined. Dereferencing an invalid pointer is undefined. The output of the code could be anything. A compiler is not required to diagnose undefined behavior, though there are features and tools that can help. With gcc it is -fsanitize=address that detects the issue in both your codes: https://godbolt.org/z/osc9ah1jo and https://godbolt.org/z/334qaKzb9.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
1

Why is this not happening?

The program(both snippet 1 and snippet 2) have undefined behavior. In the first snippet the object a has been destroyed once it goes out of scope and so ptr becomes a dangling pointer. And dereferencing that dangling pointer(which you do by writing the expression ptr->value) is undefined behavior.

And in the second snippet, the temporary string is destroyed once the constructor call finishes. So, again a.str is a dangling pointer. And dereferencing this dangling pointer(which you did by writing the expression *a.str) is undefined behavior.

Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior.

So the output that you're seeing(maybe seeing) is a result of undefined behavior. And as i said don't rely on the output of a program that has UB. The program may just crash.

For example, here the program seems to give correct output but here it gives garbage output.

So the first step to make the program correct would be to remove UB. Then and only then you can start reasoning about the output of the program.


1For a more technically accurate definition of undefined behavior see this where it is mentioned that: there are no restrictions on the behavior of the program.

Jason
  • 36,170
  • 5
  • 26
  • 60