Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I believe this is a bug in clang related to a placement new-expression whose constructor throws

The problem arises when a new-expression of the form new(std::nothrow) C; where C is a class name whose constructor throws. See the code below and the live example using g++:

#include <iostream>

void* operator new(std::size_t size, const std::nothrow_t&) noexcept
{
    void* p;
    p = malloc(size);
    std::cout << "operator new(std::nothrow)" << '\n';
    return p;
}

void operator delete(void* p, const std::nothrow_t&) noexcept
{
    free(p);
    std::cout << "operator delete(std::nothrow)" << '\n';
    std::cout << p << '\n';
}

class T{};

class C {
    int i;
public:
    C(int i) : i{i} { std::cout << "C()" << '\n'; throw T{}; }
    ~C() { std::cout << "~C()" << '\n'; }
};

int main()
{
    C* c;
    try { c = new(std::nothrow) C(3); }
    catch (T&)
    {
        std::cout << "exception thrown in C(int) was caught" << '\n';
        std::cout << c << '\n';
    }
}

g++ prints the following and it seems to be correct:

operator new(std::nothrow)
C()
operator delete(std::nothrow)
0x13f9c20
exception thrown in C(int) was caught
0

Whereas, if you use clang, you'll get the following output:

operator new(std::nothrow)
C()
exception thrown in C(int) was caught
0x7fffecdeed00

That is, it seems like clang is not invoking the operator delete(void*, std::nothrow_t&) defined in the program, and is calling instead, the operator in the standard library.

The weird thing is that, by just deleting the expression std::cout << p << '\n'; in the operator delete(void*, std::nothrow_t&), defined in the code, clangs appears to execute correctly, printing:

operator new(std::nothrow)
C()
operator delete(std::nothrow)
exception thrown in C(int) was caught
0x7fffc0ffc000

Edit

In response to the comment by @T.C. below and to the others who say that the code above has undefined behavior, I present below another code that shows how the compiler should act, to correctly compile the snippet above, using the pseudo-code presented by @T.C. here. See also this live example. An important point to note, is that this code does not use the new-expression new(nothrow).

#include <iostream>

void * operator new(std::size_t n)
{
    void* p;
    try { p = malloc(n); }
    catch (std::bad_alloc&) { throw; }
    std::cout << "operator new" << '\n';
    return p;
}

void operator delete(void *p) noexcept
{
    free(p);
    std::cout << "operator delete" << '\n';
}

void* operator new(std::size_t size, const std::nothrow_t&) noexcept
{
    void* p = malloc(size);
    std::cout << "operator new(std::nothrow)" << '\n';
    return p;
}

void operator delete(void* p, const std::nothrow_t&) noexcept
{
    free(p);
    std::cout << "operator delete(std::nothrow)" << '\n';
    std::cout << p << '\n';
}

class T {};

class C {
    int i;
public:
    C(int i) : i{ i } { std::cout << "C()" << '\n'; throw T{}; }
    ~C() { std::cout << "~C()" << '\n'; }
};

int main()
{
    C *c;
    try
    {
        c = (C*)operator new(sizeof(C), std::nothrow);
        struct cleanup
        {
            void* p;
            bool active;
            ~cleanup() { if (active) operator delete(p, std::nothrow); }
            void dismiss() { active = false; }
        } guard = { (void*)c, true };
        new(c) C{1};
        guard.dismiss();
    }
    catch ( std::bad_alloc& ) { c = nullptr; }
    catch (T&)
    {
        std::cout << "exception thrown in C() was caught" << '\n';
        std::cout << c << '\n';
    }
}

g++ prints the following for this code:

operator new(std::nothrow)
C()
operator delete(std::nothrow)
0x10c3c20
exception thrown in C() was caught
0x10c3c20

Surprisingly, clang appears to act correctly with this code, that doesn't use the new-expression new(nothrow), which clearly shows that clang has a bug while compiling this new-expression.

like image 764
Belloc Avatar asked Dec 06 '15 17:12

Belloc


1 Answers

On my system, OS X 10.11.1, the std::lib-supplied operator delete is in /usr/lib/libc++abi.dylib. On Unix-like systems, this signature is made replaceable by giving it "weak linkage". When the linker sees two identical signatures, and one of them has weak linkage, it will prefer the one that doesn't.

I can confirm that on my system, operator delete(void*, std::nothrow_t const&) has weak linkage with the following command:

$ nm -gm /usr/lib/libc++abi.dylib |c++filt |grep nothrow_t
0000000000024406 (__TEXT,__text) weak external operator delete[](void*, std::nothrow_t const&)
00000000000243fc (__TEXT,__text) weak external operator delete(void*, std::nothrow_t const&)
00000000000243c0 (__TEXT,__text) weak external operator new[](unsigned long, std::nothrow_t const&)
000000000002437e (__TEXT,__text) weak external operator new(unsigned long, std::nothrow_t const&)

Can you do the analogous analysis on your system and report the results?

Update

Thanks to T.C.'s instructions below on how to replicate the symptom, it now looks to me like this is a clang compiler code generation bug, introduced in 3.7, still present in tip-of-trunk, and only reproducible at -O2 (not -O1 or lower and not -O3).

I think a bug report is in order, and it should have good instructions on how to reproduce the bug (unless you want them to give this a low priority).

PS

And set C *c = nullptr; so that they don't waste time chasing irrelevant UB.

2nd Update

I still can not reproduce this locally with clang tip-of-trunk. But I can see it on websites such as:

http://melpon.org/wandbox/permlink/5zIRyPJpq32LfU0t

I do not yet have an explanation for this discrepancy. Perhaps my tip-of-trunk is more recent than theirs? Perhaps they are not using libc++abi?

like image 57
Howard Hinnant Avatar answered Oct 03 '22 17:10

Howard Hinnant