1

Background

I have got a network like setting with nodes and edges. Both nodes and edges need to be classes, in this case Node or Arc, as in this question. In my real setup I am dealing with quite a number of subclasses of both Node and Arc. For memory management, I use this answer to the question above.

Problem

When the constructor throws an exception, Visual Studio and g++ with MinGW on Windows cannot catch it, but exit without error handling (g++/MinGW reporting a SIGTRAP signal) while g++ and clang++ on Linux handle the exception correctly. If the Arc is created without exception Arc(n1, n2, false), all compilers work fine. In all cases, there are no relevant compiler warnings (using /W4 resp. -Wall) Can someone explain me, why this is not working on Windows? Or even give a workaround?

Code

#include <iostream>
#include <stdexcept>
#include <vector>
#include <memory>

struct Node;
struct Arc {
    Node *left,*right;
private:
    // shared pointer to self, manages the lifetime.
    std::shared_ptr<Arc> skyhook{this};
public:
    // c'tor of Arc, registers Arc with its nodes (as weak pointers of skyhook)
    explicit Arc(Node* a_, Node* b_, bool throw_exc);
    // resets skyhook to kill it self
    void free() {
        std::cout << "  Arc::free();\n" << std::flush;
        skyhook.reset();
    }
    virtual ~Arc() {
        std::cout << "  Arc::~Arc();\n" << std::flush;
    }
};

struct Node {
    explicit Node() {
        std::cout << "  Node::Node()\n" << std::flush;
    }
    std::vector<std::weak_ptr<Arc> > arcs;
    ~Node() {
        std::cout << "  Node::~Node();\n" << std::flush;
        for(const auto &w : arcs) {
            if(const auto a=w.lock()) {
                a->free();
            }
        }
    }
};

Arc::Arc(Node *a_, Node *b_, bool throw_exc) : left(a_), right(b_) {
    std::cout << "  Arc::Arc()\n" << std::flush;
    if (throw_exc) {
        throw std::runtime_error("throw in Arc::Arc(...)");
    }
    a_->arcs.push_back(skyhook);
    b_->arcs.push_back(skyhook);

}

int main(int argc, char* argv[]) {
    std::cout << "n1=new Node()\n" << std::flush;
    Node *n1 = new Node();
    std::cout << "n2=new Node()\n" << std::flush;
    Node *n2 = new Node();
    std::cout << "try a=new Arc()\n" << std::flush;
    try {
        Arc *a = new Arc(n1, n2, true);
    } catch (const std::runtime_error &e) {
        std::cout << "Failed to build Arc: " << e.what() << "\n" << std::flush;
    }
    std::cout << "delete n1\n" << std::flush;
    delete n1;
    std::cout << "delete n2\n" << std::flush;
    delete n2;

}

Output

This is what I get both on Linux as well on Windows

n1=new Node()
  Node::Node()
n2=new Node()
  Node::Node()
try a=new Arc()
  Arc::Arc()

With g++ (7.4.0 and 8.3.0) or clang++ (6.0.0) on Linux ...

it works as expected:

  Arc::~Arc();
Failed to build Arc: throw in Arc::Arc(...)
delete n1
  Node::~Node();
delete n2
  Node::~Node();

With VC++ (2017) ...

it breaks

Arc::~Arc()

and the run terminates with exit code -1073740940 (0xC0000374)

with g++ (9.1.0) MinGW 7.0

it breaks, but reports the signal

Signal: SIGTRAP (Trace/breakpoint trap)
  Arc::~Arc();

And finishes with exit code 1

oekopez
  • 1,080
  • 1
  • 11
  • 19
  • 1
    Which [exception handling model](https://learn.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model) are you using? – You Jun 25 '19 at 09:27
  • The exception handling model is /EHsc – oekopez Jun 25 '19 at 09:40
  • 1
    Adding try/catch in the constructor can handle the exception. tested on vc++2019. – Sumit Kumar Jun 25 '19 at 10:03
  • Side note: Holding a `std::shared_ptr` to `this` is not going to be a stellar idea; you should probably use [`std::enable_shared_from_this`](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this) instead. I suspect this is what's causing your issues, but I can't find any specific undefined behavior to call you out on in an answer... – You Jun 25 '19 at 10:17
  • it is possible to try/catch inside a constructor to handle failures. For example, if you construct two objects in your constructor, and the second one throws bad_alloc, what do you do? Catching the exception in the calling code will leak the first object. Handling inside the constructor avoids this. – Sumit Kumar Jun 25 '19 at 12:38
  • 1
    @user2181624: Note that if a constructor exits via an exception, all constructed members and bases (and local variables, of course) will be destroyed even though (for a non-delegating constructor) the class’s destructor is not called—it is therefore often possible to avoid any explicit exception handling in the constructor. – Davis Herring Jun 25 '19 at 12:58
  • @You: As the promulgator of the `shared_ptr` in question: we don’t have to worry about `enable_shared_from_this` because we *always* create a `shared_ptr` from which all ownership of the object derives. – Davis Herring Jun 25 '19 at 13:01
  • 2
    @DavisHerring: I'd argue that it's still very fragile because 1) move/copy has to be carefully handled (and it isn't, here), 2) creating instances on the stack will be syntactically possible but near-guaranteed undefined behavior (unless `free` is manually called), and 3) doing `auto* p = new Arc(...); delete p;` is *also* guaranteed UB (double free, again if `free` is not called). It's a giant foot-gun. – You Jun 25 '19 at 13:10
  • @You: Your concerns are valid, although private constructors can alleviate most of them (as is common for “heap-only” types—not that those are always the best idea). – Davis Herring Jun 25 '19 at 14:13
  • @You: The library where this approach should be used is mainly used as a CPython extension with a wrapper interface made with SWIG. Hence: No stack objects and the possibility to tell Python that Arc is managed by C++. My original solution for the network ownership problem is different (shared_ptr in the nodes and weak_ptr in the Arc) and uses shared_from_this and has the same problem (and the same benefits as long as no Arc constructor throws). This original solution has worked for years, until I wanted to throw in the constructor of an Arc. – oekopez Jun 25 '19 at 14:22
  • That's odd; using `shared_from_this` *should* work better (assuming any exceptions are thrown *before* the any shared pointers are created). The issue with your current code, as far as I can see, is that your `shared_ptr` member is fully constructed before the exception is thrown, but will refer to an object that is not fully constructed (yet). It *will* still call the destructor of this object upon destruction, which is undefined behavior IIRC. Defering assignment of the shared pointer to the end of the constructor could help, but as you've noticed this is very fragile. – You Jun 25 '19 at 14:49
  • [SEH](https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=vs-2019) is intrinsic to Win. Somewhere at the top (hint: main()), [on Windows builds, make sure you catch the SE raised](https://learn.microsoft.com/en-us/windows/win32/debug/using-an-exception-handler). And from there [generate a minidump](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump). not that complicated as it sounds) You open that minidump file with Visual Studio and then do the "native debugging" .. that wil pin-point the issue. – Chef Gladiator Oct 05 '20 at 13:05
  • Good software Architect has always three options. Which two have you discarded? – Chef Gladiator Oct 05 '20 at 13:18

3 Answers3

4

tl;dr: inherit from std::enable_shared_from_this and use weak_from_this().


Consider the following structure, which is similar to yours (https://godbolt.org/z/vHh3ME):

struct thing
{
  std::shared_ptr<thing> self{this};

  thing()
  {
    throw std::exception();
  }
};

What is the state of the objects *this and self at the moment the exception is thrown, and which destructors are going to be executed as part of stack unwinding? The object itself has not yet finished constructing, and therefore ~thing() will not (and must not) be executed. On the other hand, self is fully constructed (members are initialized before the constructor body is entered). Therefore, ~std::shared_ptr<thing>() will execute, which will call ~thing() on an object which is not fully constructed.

Inheriting from std::enable_shared_from_this does not exhibit this problem assuming no actual shared_ptrs are created before the constructor finishes executing and/or throws (weak_from_this() would be your friend here), since it only holds a std::weak_ptr (https://godbolt.org/z/TGiw2Z); neither does a variant where your shared_ptr is initialized at the end of the constructor (https://godbolt.org/z/0MkwUa), but that's not trivial to incorporate in your case since you're giving shared/weak pointers away in the constructor.

That being said, you still have an ownership problem. Nobody actually owns your Arc; the only outside references to it are weak_ptrs.

You
  • 22,800
  • 3
  • 51
  • 64
2

It looks like std::shared_ptr is used here to avoid thinking about lifetimes and ownership, which leads to poor code.

A better design is to have a class, say Network, that owns Nodes and Arcs and stores them in std::list. This way you don't need std::shared_ptr or std::week_ptr and the convoluted code that results from using those. Nodes and Arcs can just use plain pointers to each other.

Example:

#include <list>
#include <vector>
#include <cstdio>

struct Node;

struct Arc {
    Node *left, *right;
};

struct Node {
    std::vector<Arc*> arcs;
};

class Network {
    std::list<Node> nodes;
    std::list<Arc> arcs;

public:
    Node* createNode() {
        return &*nodes.emplace(nodes.end());
    }

    Arc* createArc(Node* left, Node* right) {
        Arc* arc = &*arcs.emplace(arcs.end(), Arc{left, right});
        left->arcs.push_back(arc);
        right->arcs.push_back(arc);
        return arc;
    }
};

int main() {
    Network network;
    Node* a = network.createNode();
    Node* b = network.createNode();
    Arc* ab = network.createArc(a, b);
    std::printf("%p %p %p\n", a, b, ab);
}
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • Thank you for the answer. The problem is, that ownership is in my real code complicated: the code is a C++ Library mostly used as a CPython extension wrapped with SWIG. The users are Python writing scientists who should not be bothered with ownership. I have to solve that in the C++ code. Going this way would change the whole API of a large existing framework. If no other option comes up, I need to go this way. – oekopez Jun 25 '19 at 12:29
  • 1
    @oekopez it sounds like you want a garbage collector, not `std::shared_ptr` – Caleth Jun 25 '19 at 16:03
2

(It took me a few minutes to realize my own comments were the answer…)

The problem here is that the shared_ptr is (fully) constructed before the Arc is; if an exception interrupts the Arc construction, its destructor is not supposed to be called, but destroying skyhook calls it anyway. (It is legitimate to delete this, even indirectly, but not in this context!)

Since it’s impossible to release a shared_ptr without trickery, the simplest thing to do is to provide a factory function (which avoids certain other problems):

struct Arc {
  Node *left,*right;
private:
  std::shared_ptr<Arc> skyhook;  // will own *this
  Arc(Node *l,Node *r) : left(l),right(r) {}
public:
  static auto make(Node*,Node*);
  void free() {skyhook.reset();}
};
auto Arc::make(Node *l,Node *r) {
  const auto ret=std::make_shared<Arc>(l,r);
  ret->left->arcs.push_back(ret);
  ret->right->arcs.push_back(ret);
  ret->skyhook=ret;  // after securing Node references
  return ret;
}

Since constructing a shared_ptr has to allocate, this is already necessary if you’re concerned about bad_alloc at all.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76