【问题标题】:Boost Graph Library: Check validity of vertex_descriptor in a performant wayBoost Graph Library:以高效的方式检查 vertex_descriptor 的有效性
【发布时间】:2019-08-23 10:50:50
【问题描述】:

BGL 旨在尽可能通用。因此,我为另一款软件编写了一个接口,该软件也旨在尽可能通用。但是,当 vertex_descriptor 无效时,我会遇到问题。

这是一个描述问题的最小示例:

template <class Graph>
class Wrapper {
  public:
    T& get_property_obj(const typename Graph::vertex_descriptor& v) {
        // This should be inserted:
        // if (invalid_descriptor(v)) {
        //    throw something;
        // }
        return g[v];
    }
  private:
    Graph& g;
};

问题在于,当Graphboost::adjacency_list 并以vecT 作为顶点存储类型时,Graph::operator[ 似乎不会检查v 是否有效并且可以提供无效内存。

我能否以某种方式在包装类中简单检查v 是否有效?

显然,最简单的解决方案是遍历所有顶点并检查是否相等,但对于给定的示例,检查 vertex_descriptornum_vertices() 就足够了(但当然不是通用的)。

【问题讨论】:

    标签: c++ boost boost-graph


    【解决方案1】:

    没有办法判断一个顶点描述符是否有效

    你只能检查

    • 范围(在积分顶点描述符的情况下,如 vecS)
    • 通过查找基于节点的顶点存储描述符。

    但是,这两种情况都有可能给您带来错误的结果,原因与标准库容器相同:

     std::vector<int> is{1,2,3};
     auto i1 = is.begin();
    
     is.push_back(4);
    
     std::cout << "Undefined Behaviour: " << *i1;
    

    关键是,迭代器(或描述符,视情况而定)已失效。无法检测到这种情况发生了¹,您将始终必须自己处理。

    迭代器/描述符失效保证遵循底层容器的保证。这意味着对于基于节点的容器,您实际上可以依赖描述符(和引用)在插入甚至删除过程中保持稳定(显然,除了对于被删除的元素)。

    参见例如Iterator invalidation rules

    所以对于整数描述符,你会写:

    bool descriptor_looks_valid(vertex_descriptor v) const {
        return v>=0 && v < num_vertices(g);
    }
    

    如您所知,对于大多数其他容器选择器来说,它的效率会非常低:

    bool descriptor_looks_valid(vertex_descriptor v) const {
        auto vds = vertices(g);
        return std::count(vds.first, vds.second, v);
    }
    

    或者一般来说(假设 c++17):

    bool descriptor_looks_valid(vertex_descriptor v) const {
        if constexpr(std::is_integral_v<vertex_descriptor>) {
            return v>=0 && v < num_vertices(g);
        } else {
            auto vds = vertices(g);
            return std::count(vds.first, vds.second, v);
        }
    }
    

    危险示范

    这个小演示展示了将“范围检查通过”误认为“有效”的危险。这个程序重复这个:

    template <typename vertexS> void doTest() {
        using Graph = boost::adjacency_list<
            boost::vecS,
            vertexS,
            boost::directedS,
            PropertyObj>;
    
        Graph g;
        auto v1 = add_vertex({"one"}, g);
        auto v2 = add_vertex({"two"}, g);
        auto v3 = add_vertex({"three"}, g);
        auto v4 = add_vertex({"four"}, g);
        auto v5 = add_vertex({"five"}, g);
    
        Wrapper w = g;
        std::cout << w.get_property_obj(v3).something << std::endl;
    
        // but this is confounding, and only accidentally "works" for vecS:
        remove_vertex(v1, g);
        std::cout << w.get_property_obj(v3).something << std::endl;
    
        try {
            // this looks "valid" with vecS, but should throw for listS
            //
            // of course, like with v3 the descriptor was already invalidated both cases
            std::cout << w.get_property_obj(v1).something << std::endl;
        } catch(std::range_error const& re) {
            std::cout << "(range_error cautgh: " << re.what() << "\n";
        }
    }
    

    对于vertexS 等于vecSlistSsetS。典型输出为 Live On Coliru

    Testing with vecS:
    three
    four
    two
    
    Testing with listS:
    three
    three
    (range_error caught: get_property_obj
    
    Testing with setS:
    three
    three
    (range_error caught: get_property_obj
    

    结论

    没有实现有效性检查的原因是底层容器不支持它们。

    此外,尽管您可以“近似”验证,但这只会防止崩溃,而不是防止未指定的行为。

    事实上,根据预期的语义,您可能会触发相同的Undefined Behavior(例如,如果您假设get_property_obj(v3) 每次都产生相同的值,那么您将使用vecS 破坏代码)。

    我能否以某种方式在包装类中简单检查 v 是否有效?

    简而言之,不。描述符有效性是使用模式的函数,调用者必须考虑到它。

    完整列表

    Live On Coliru

    #include <boost/graph/adjacency_list.hpp>
    #include <iostream>
    
    struct PropertyObj {
        std::string something;
    };
    
    template <class Graph, class T = PropertyObj>
    class Wrapper {
      public:
        using vertex_descriptor = typename Graph::vertex_descriptor;
        T& get_property_obj(vertex_descriptor v) {
            if (!descriptor_looks_valid(v)) 
                throw std::range_error("get_property_obj");
            return g[v];
        }
        Wrapper(Graph& g) : g(g){}
      private:
        bool descriptor_looks_valid(vertex_descriptor v) const {
            if constexpr(std::is_integral_v<vertex_descriptor>) {
                return v>=0 && v < num_vertices(g);
            } else {
                auto vds = vertices(g);
                return std::count(vds.first, vds.second, v);
            }
        }
    
        Graph& g;
    };
    
    template <typename vertexS> void doTest() {
        using Graph = boost::adjacency_list<
            boost::vecS,
            vertexS,
            boost::directedS,
            PropertyObj>;
    
        Graph g;
        auto v1 = add_vertex({"one"}, g);
        auto v2 = add_vertex({"two"}, g);
        auto v3 = add_vertex({"three"}, g);
        auto v4 = add_vertex({"four"}, g);
        auto v5 = add_vertex({"five"}, g);
    
        boost::ignore_unused_variable_warning(v1);
        boost::ignore_unused_variable_warning(v2);
        boost::ignore_unused_variable_warning(v3);
        boost::ignore_unused_variable_warning(v4);
        boost::ignore_unused_variable_warning(v5);
    
        Wrapper w = g;
        std::cout << w.get_property_obj(v3).something << std::endl;
    
        // but this is confounding, and only accidentally "works" for vecS:
        remove_vertex(v1, g);
        std::cout << w.get_property_obj(v3).something << std::endl;
    
        try {
            // this looks "valid" with vecS, but should throw for listS
            //
            // of course, like with v3 the descriptor was already invalidated both cases
            std::cout << w.get_property_obj(v1).something << std::endl;
        } catch(std::range_error const& re) {
            std::cout << "(range_error caught: " << re.what() << "\n";
        }
    }
    
    int main() {
        std::cout << "Testing with vecS:\n";
        doTest<boost::vecS>();
    
        std::cout << "\nTesting with listS:\n";
        doTest<boost::listS>();
    
        std::cout << "\nTesting with setS:\n";
        doTest<boost::setS>();
    }
    

    ¹尽管某些库实现具有允许您在某些时候检测到它的扩展 - 例如 https://docs.microsoft.com/en-us/cpp/standard-library/debug-iterator-support?view=vs-2019

    【讨论】:

    • 注意:std::count 的使用是为了简洁而不是尽可能高效
    猜你喜欢
    • 1970-01-01
    • 2015-04-28
    • 1970-01-01
    • 2012-11-24
    • 2011-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多