Have I misunderstood the scope of this default argument shared_ptr?
Asked Answered
P

2

7

Take a look at this:

#include <iostream>
#include <memory>

using Foo = int;
using FooPtr = std::shared_ptr<Foo>;

FooPtr makeFoo()
{
    FooPtr f{
        new Foo(),
        [](Foo* ptr) {
            delete ptr;

            std::cerr << "!\n";
        }
    };

    return f;
}

void bar(FooPtr p = {})
{
    p = makeFoo();
}

int main()
{
    bar();
}

// Expected output: '!'
// Failure case: no output (deleter not invoked?)

I expected the shared_ptr deleter to be called when bar() returns, and on my 64-bit CentOS 7 system using GCC 4.8.5, it does.

However, on my 32-bit CentOS 6 system using GCC 4.8.2 under devtoolset-2 (also I think under gcc-linaro-arm-linux-gnueabihf-4.8-2013.10_linux, my Raspberry Pi toolchain), it doesn't.

Looking at the code, and given C++11's experimental nature in 4.8, this smells like a compiler bug to me. But I could also be falling into a UB trap somewhere (or just generally misunderstanding how this stuff ought to work).

Who's at fault? And how should I fix it?


Works on

Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)

Fails on

Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-2/root/usr/libexec/gcc/i686-redhat-linux/4.8.2/lto-wrapper
Target: i686-redhat-linux
Configured with: ../configure --prefix=/opt/rh/devtoolset-2/root/usr --mandir=/opt/rh/devtoolset-2/root/usr/share/man --infodir=/opt/rh/devtoolset-2/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --enable-languages=c,c++,fortran,lto --enable-plugin --with-linker-hash-style=gnu --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.2-20140120/obj-i686-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.2-20140120/obj-i686-redhat-linux/cloog-install --with-mpc=/builddir/build/BUILD/gcc-4.8.2-20140120/obj-i686-redhat-linux/mpc-install --with-tune=generic --with-arch=i686 --build=i686-redhat-linux
Thread model: posix
gcc version 4.8.2 20140120 (Red Hat 4.8.2-15) (GCC)
Pyridine answered 26/3, 2018 at 18:17 Comment(16)
Are you sure this has anything to do with the default argument, and not just with the copy-capture?Fauteuil
I have been looking through libstdc++'s Bugzilla and will continue to do so but mehPyridine
@KerrekSB: Good question. The = is not necessary in this MCVE. I have reproduced again without and will update the question. (tl;dr: yes)Pyridine
@LightnessRacesinOrbit might change the subject and tags as well :)Bushido
Also void bar() { auto p = makeFoo(); } behavesPyridine
What do you mean 'behaves'? As differentiated from what?Bushido
@SergeyA: It produces the expected output. This is why I believe the default argument is where my confusion (or a libstdc++ bug) lies.Pyridine
Why are you not using std::make_shared??Vibratory
@JesperJuhl: Can std::make_shared accept a deleter?Pyridine
@Lightness Races in Orbit - whoops, good point.Vibratory
Do you still want an answer that tells you the lifetime of the parameters or would you just like the section in the standard?Maxey
@NathanOliver: Depends how much rep you want ;)Pyridine
void bar(FooPtr p) {} void bar() { auto p = makeFoo(); bar(p); } int main() { bar(); } yields the expected output. Guess that's my fix/workaroundPyridine
What's the point of an argument that you immediately overwrite?Castro
@Barmar: It's an MCVE; in reality the argument is only conditionally assigned-to, but that facet is not required to reproduce the observed problem.Pyridine
@LightnessRacesinOrbit Sometimes a CVE can be too M. :)Castro
P
4

As Nathan has shown, my assumptions about the pointer's lifetime were standard-correct.

That the deleter isn't being invoked does appear to be a GCC or libstdc++ bug, possibly bug 60367 given that the linked comment solves it, the symptoms seem similarish and it was fixed before GCC 4.8.5.

Replacing = {} with = FooPtr{} appears to be a viable workaround.


Note well, there is also a regression in 7.2 and some older "8.0" trunk builds that may cause bad behaviour in similar circumstances (thanks Arne Vogel!).

Pyridine answered 26/3, 2018 at 19:15 Comment(4)
There is also a regression in 7.2 and 8.0 that may cause bad behavior in some newer compiler versions.Morphophonemics
@ArneVogel: Oh lordy! Well at least I don't have to worry about such newfangled versions ^_^Pyridine
I think you're right that it's bug 60367 (that's the one I thought of as soon as I saw OP's post). GCC 4.8.3 gives the right answer, so that lines up with it being that bug too.Obstruction
@JonathanWakely: Great, thanks for confirming (fsvo "confirm")Pyridine
M
5

The destructor should be called either when bar returns or and the end of the full expression in which bar is called.

If we look at [expr.call]/4 (C++17 draft) we have

When a function is called, each parameter (11.3.5) shall be initialized (11.6, 15.8, 15.1) with its corresponding argument.[...]It is implementation-defined whether the lifetime of a parameter ends when the function in which it is defined returns or at the end of the enclosing full-expression.[...]

So p should be initialized to a null FooPtr at the start of the function, move assigned the return of MakeFoo, and then finally destroyed (in turn calling the deleter) at the end of bar or after bar returns in main.

Maxey answered 26/3, 2018 at 18:44 Comment(13)
Okay yeah, or in C++11 the very similar "The lifetime of a parameter ends when the function in which it is defined returns. The initialization and destruction of each parameter occurs within the context of the calling function". I'm happy to conclude that this is a GCC bug then but have still not been able to find a bug # to reference :(Pyridine
@LightnessRacesinOrbit Out of curiosity, on the broken build, if you do std::get_deleter(p) after p = makeFoo(); does it return nullptr?Maxey
It doesn't even compile and I can't work out why. Oh apparently I need to give it the Deleter's type hang onPyridine
Yes, it seems to be nullptrPyridine
std::cerr << std::get_deleter<void(*)(Foo*)>(p) << '\n'; => 0Pyridine
@LightnessRacesinOrbit Defenitly sounds like a bug where the deleter is not being moved into p.Maxey
Yarp. I'd probably be content with working around it and calling it a day, except our main target is the 64-bit CentOS 7 and I'd like to be able to point folks to proof that only the 32-bit targets are affected (if, indeed, that is truly the case). Because it seems a bit odd for the bitness to matter that much, though ultimately it is a different toolchainPyridine
I imagine the real difference is between GCC 4.8.2 and 4.8.5.Pyridine
@LightnessRacesinOrbit Probably. I'm trying to find a bug report but not having much luckMaxey
Same. Have at least confirmed that CentOS 6 64-bit with GCC 4.8.2 (also devtoolset-2) is affected the same.Pyridine
Inspired by gcc.gnu.org/bugzilla/show_bug.cgi?id=60367#c4 I changed = {} to = FooPtr{} and that yields the expected behaviour. Lol.Pyridine
@LightnessRacesinOrbit Good find. Looks like that might be it. Says it is fixed in 4.8.3 too.Maxey
Yep I think that'll have to do for tonight :) Thanks for your help!Pyridine
P
4

As Nathan has shown, my assumptions about the pointer's lifetime were standard-correct.

That the deleter isn't being invoked does appear to be a GCC or libstdc++ bug, possibly bug 60367 given that the linked comment solves it, the symptoms seem similarish and it was fixed before GCC 4.8.5.

Replacing = {} with = FooPtr{} appears to be a viable workaround.


Note well, there is also a regression in 7.2 and some older "8.0" trunk builds that may cause bad behaviour in similar circumstances (thanks Arne Vogel!).

Pyridine answered 26/3, 2018 at 19:15 Comment(4)
There is also a regression in 7.2 and 8.0 that may cause bad behavior in some newer compiler versions.Morphophonemics
@ArneVogel: Oh lordy! Well at least I don't have to worry about such newfangled versions ^_^Pyridine
I think you're right that it's bug 60367 (that's the one I thought of as soon as I saw OP's post). GCC 4.8.3 gives the right answer, so that lines up with it being that bug too.Obstruction
@JonathanWakely: Great, thanks for confirming (fsvo "confirm")Pyridine

© 2022 - 2024 — McMap. All rights reserved.