当网络边缘的构造函数抛出时避免 SIGTRAP

Avoid SIGTRAP when the constructor of a network edge throws

背景

我有一个类似设置节点和边的网络。节点和边都需要 类,在本例中为 NodeArc. In my real setup I am dealing with quite a number of subclasses of both Node and Arc. For memory management, I use

问题

当构造函数抛出异常时,Visual Studio 和g++ with MinGW on Windows 无法捕捉到它,但没有错误处理退出(g++/MinGW 报告SIGTRAP 信号),而g++ and clang++ on Linux 正确处理异常。如果 Arc 无一例外地创建 Arc(n1, n2, false),所有编译器都可以正常工作。在所有情况下,都没有相关的编译器警告(使用 /W4 resp.-Wall)有人可以解释一下,为什么这在 Windows 上不起作用?或者甚至给出解决方法?

代码

#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;

}

输出

这是我在 Linux 和 Windows

上得到的
n1=new Node()
  Node::Node()
n2=new Node()
  Node::Node()
try a=new Arc()
  Arc::Arc()

在 Linux ...

上使用 g++(7.4.0 和 8.3.0)或 clang++(6.0.0)

它按预期工作:

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

与 VC++ (2017) ...

它坏了

Arc::~Arc()

并且 运行 以退出代码 -1073740940 (0xC0000374)

终止

使用 g++ (9.1.0) MinGW 7.0

它坏了,但报告了信号

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

并以退出代码 1 结束

这里似乎使用了 std::shared_ptr 以避免考虑生命周期和所有权,这会导致代码不佳。

更好的设计是有一个 class,比如 Network,它拥有 NodeArc 并将它们存储在 std::list 中。这样您就不需要 std::shared_ptrstd::week_ptr 以及使用它们产生的复杂代码。 Nodes 和 Arcs 只能使用指向彼此的普通指针。

示例:

#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);
}

tl;dr: 继承自 std::enable_shared_from_this 并使用 weak_from_this().


考虑以下结构,它与您的 (https://godbolt.org/z/vHh3ME) 相似:

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

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

在抛出异常时对象 *thisself 的状态是什么,哪些析构函数将作为堆栈展开的一部分执行?对象本身还没有完成构造,因此 ~thing() 不会(也不能)被执行。另一方面,self 完全构造的(成员在进入构造函数体之前被初始化)。因此,~std::shared_ptr<thing>() 执行,这将在未完全构造的对象上调用 ~thing()

std::enable_shared_from_this 继承不会出现此问题,假设在构造函数完成执行 and/or throws 之前没有创建实际的 shared_ptrweak_from_this() 将是您的朋友) ), 因为它只包含一个 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),但是在你的案例中合并这并不容易,因为你给出了 shared/weak 指针 in构造函数。

话虽如此,您仍然有所有权问题。没有人真正拥有你的 Arc;对它的唯一外部引用是 weak_ptrs.

(我花了几分钟才意识到我自己的评论就是答案……)

这里的问题是 shared_ptr 是在 Arc 之前(完全)构建的;如果异常中断 Arc 构造,则不应调用其析构函数,但销毁 skyhook 无论如何都会调用它。 (它 delete this 是合法的,甚至是间接合法的,但不是在这种情况下!)

因为是 impossible to release a shared_ptr without trickery, the simplest thing to do is to provide a factory function (which avoids ):

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;
}

因为构造一个shared_ptr必须分配,如果你关心bad_alloc,这已经是必要的了。