Home > front end >  strange behavior for accessing the return value in a destructor in c ?
strange behavior for accessing the return value in a destructor in c ?

Time:07-04

here is the code:

expected behavior: In ~X() the ptr shoule be != nil no matter the IS_ERR macro is defined or not

#include <iostream>
#include <functional>
#include <vector>
#include <memory>

struct X {
    X(std::function<void()> f): f_{std::move(f)} {}

    ~X() {
        f_();
    }

    std::function<void()> f_;
};

std::shared_ptr<int> func() {
    std::shared_ptr<int> ptr;

    X x([&] {
        std::cout << "dtor, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
    });

    if (ptr = std::make_shared<int>(100)) {
        std::cout << "return, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
        return ptr;
    }

#if defined(IS_ERR)
    return nullptr;
    // return, ptr == null? 0, addr 140732821766368
    // dtor, ptr == null? 1, addr 140732821766368 // ATTENTION: ptr == nil here !!!! the internal of ptr is moved to the caller before the destructor of X is executed even if there is no receiver in the caller. 
    // fini, ptr == null? 0, addr 140732821766576
#else
    // return, ptr == null? 0, addr 140732879315376
    // dtor, ptr == null? 0, addr 140732879315376 // ATTENTION ptr != nil  the expected behavior
    // fini, ptr == null? 0, addr 140732879315376
    return ptr;
#endif
}

int main()
{
    auto ptr = func();
    std::cout << "fini, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
}

  • in the defined(IS_ERR)

// ATTENTION: ptr == nil here !!!! the internal of ptr is moved to the caller before the destructor of X is executed even if there is no receiver in the caller.

  • in the no defined(IS_ERR)

// ATTENTION ptr != nil the expected behavior

  • the code is tested under followings enviornments:
g   --version : g   (GCC) 4.8.2 20140120 (Red Hat 4.8.2-15)
Apple LLVM version 10.0.1 (clang-1001.0.46.4) Target: x86_64-apple-darwin18.7.0

the test result is the same:

rm -f test && clang   -o test test.cc -std=c  11  -Wno-parentheses && ./test | grep dtor    
dtor, ptr == null? 0, addr 140732827398560
rm -f test && clang   -o test test.cc -std=c  11    -DIS_ERR  -Wno-parentheses && ./test | grep dtor   
dtor, ptr == null? 1, addr 140732832612560
rm -f test && clang   -o test test.cc -std=c  11 -O3  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 0, addr 140732887884168
rm -f test && clang   -o test test.cc -std=c  11  -O3  -DIS_ERR  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 1, addr 140732702822624
rm -f test && g   -o test test.cc -std=c  11  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 0, addr 140735881081728
rm -f test && g   -o test test.cc -std=c  11    -DIS_ERR  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 1, addr 140722084209920
rm -f test && g   -o test test.cc -std=c  11 -O3  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 0, addr 140736717254624
rm -f test && g   -o test test.cc -std=c  11  -O3  -DIS_ERR  -Wno-parentheses && ./test | grep dtor
dtor, ptr == null? 1, addr 140735485800352
  • the use case of the example code is as follows:
using Error = std::shared_ptr<std::string>;
// go style error propagation is simulated here

Error func() {
  Error err;
 
    X x([&] {
        if (err) {
          // ATTENTION: we may do something here, like logging, alerting
        } else {
         // or we any do something REALLY REALLY IMPORTANT here, like express, transferring money ... it may cause catastrophic result ... (it is actually err != nil, but you think the result is ok ...) 
        }
    });
 err = doSomeThing()
if (err) {
  err = Wrap(err, "do func");
  return err;
}
#if defined(IS_ERR)
  return nullptr;
#else
  return err;
#endif
}  

CodePudding user response:

Basically, you're seeing the effect of Named Return Value Optimizaion

When the expression in a return expression is the last use of a named local variable, the compiler is permitted to optimize the return by turning the copy into a move. Arguably, since the lambda captures ptr by reference, the compiler really has no way of knowing that the return in the if is not in fact the last reference (the lambda will access it when X is destroyed), so perhaps NRVO should not be allowed here. But your compiler is doing it anyways.

When IS_ERR is not defined, there is clearly another reference to ptr after the one in the if, so NRVO is not done (at least for the return in the if -- the one at the end of the function probably gets NRVO)

The 'ptr == null' you're seeing is because the lambda is accessing the ptr after it has been moved from, not after it has been destroyed.

CodePudding user response:

When you have:

std::shared_ptr<int> func() {
    std::shared_ptr<int> ptr;

    X x([&] {
        std::cout << "dtor, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
    });

    if (ptr = std::make_shared<int>(100)) {
        std::cout << "return, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
        return ptr;
    }
    return nullptr;
}

The line return ptr; automatically moves from ptr (so is equivalent to return std::move(ptr);). This means it is moved from to construct the return value, before the destructor of X x is called, so the ptr it refers to is now empty.

The second variant without -DIS_ERR:

std::shared_ptr<int> func() {
    std::shared_ptr<int> ptr;

    X x([&] {
        std::cout << "dtor, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
    });

    if (ptr = std::make_shared<int>(100)) {
        std::cout << "return, ptr == null? " << (ptr == nullptr) << ", addr " << (long long)(&ptr) << std::endl;
        return ptr;
    }
    return ptr;
}

The function func can now employ named return value optimization (NRVO), since it always returns ptr. This means that the local variable ptr can be constructed directly in the return value (the auto ptr in the main function). The lambda in x captures the same object as the ptr in the main function, so it won't be moved from like in the previous case.

The easy fix is to capture by value in x. Otherwise, you can disable NRVO by returning std::as_const(ptr) so it can't be moved from.

  • Related