4

I usually work with vecS as container for boost::adjacency_list:

struct myVertexType { std::vector<Stuff> vec; /* and more */ };
struct myEdgeType { /* some data too */ };
using Graph = boost::adjacency_list<
        boost::vecS,
        boost::vecS,
        boost::directedS,
        myVertexType,
        myEdgeType
    >;

However, I encountered a situation were that raised an issue: I was referencing some data stored as a bundled property of a vertex and when I created another vertex, that seemed to make my reference invalid (1).

At least that's what I understood from reading this page (section "Iterator and Descriptor Stability/Invalidation").

So I switched to listS, and all went fine:

using Graph = boost::adjacency_list<
        boost::listS,
        boost::listS,
        boost::directedS,
        myVertexType,
        myEdgeType
    >;

Until...

Until I noticed that with listS, boost::target( e1, g ) fails to compile! :

Graph g;
auto e1 = boost::add_edge(1, 0, g).first;
auto t = boost::target( e1, g );

This fails to build too: (see on coliru)

Graph g;
boost::add_edge(1, 0, g);
write_graphviz(std::cout, g );

So I searched a bit and found an answer by Sehe, stating that

vecS has an implicit vertex index. listS doesn't. Therefore it uses the internal property vertex_index_t

However, the given answer uses Inner properties (?) (or is it dynamic properties?) and I am using my own datatypes for vertices and edges.

So my question is:

How can I build a list-based graph type that enables me to do all the "regular stuff" allowed by VecS?

(1) to be clear, I was referencing a vector that was in a vertex, and when I created another vertex, the vector suddenly became empty!

Edit: clarified what is inside my nodes.

kebs
  • 6,387
  • 4
  • 41
  • 70

1 Answers1

5

Background

"when I created another vertex, that seemed to make my reference invalid (1)."

Yes, that's possible.

You have to realize that there's are much bigger performance trade-offs underlying your choice of container selectors. Many algorithms can get very different efficiency characteristics.

Also, some semantics subtly change (e.g. when using setS as the edge container selector, you naturally cannot have duplicate edges anymore; this is also why add_edge returns a pair<descriptor, bool>).

Also realize that often you don't need reference or even iterator stability. The typical coding pattern in BGL is not to pass/hold references to property (bundles), but instead pass property maps by value.

Property maps abstract aways access to (mutable) properties.

You can usually pass descriptors which usually are stable (unless you're removing vertices "in the middle" in vecS, as the implied vertex index is obviously changing for all following vertices).

That said, let's move on to your problems:

Questions

Until I noticed that with listS, boost::target( e1, g ) fails to compile!

Nope. That compiles fine.

What ISN'T fine is that you call add_edge with integral arguments. The vertex descriptor isn't integral with lists/setS (node based containers).

Worse, vertices don't get automatically added for non-vecS adjacency_list so you'd be referring to vertices out-of-range anyways.

The general way to refer to these is:

V v0 = add_vertex(g);
V v1 = add_vertex(g);
auto [e1, inserted] = boost::add_edge(v0, v1, g);
assert(inserted);
[[maybe_unused]] V t = boost::target(e1, g);

The graphviz call is also fine, but fails for the same reason on add_edge...

Also, you need to add a vertex index. Either as interior property or passing a property map to the algorithm function.

Here's a complete test demo that shows all three flavours:

Live On Coliru

#include <boost/algorithm/string.hpp>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphviz.hpp>
#include <boost/core/demangle.hpp>
#include <iostream>
#include <numeric>
using boost::core::demangle;
using boost::algorithm::replace_all_copy;

struct myVertexType { /* some data */ };
struct myEdgeType { /* some data too */ };

template <typename containerS> void tests() {
    using Graph = boost::adjacency_list<
        containerS, containerS,
        boost::directedS,
        myVertexType,
        myEdgeType>;

    using V = typename Graph::vertex_descriptor;

    std::cout << "\n"
              << std::boolalpha << "tests() with "
              << demangle(typeid(containerS).name()) << " - "
              << "vertex_descriptor integral? " << std::is_integral<V>()
              << "\n";
    Graph g;

    V v0 = add_vertex(g);
    V v1 = add_vertex(g);
    auto [e1, inserted] = boost::add_edge(v0, v1, g);
    assert(inserted);
    [[maybe_unused]] V t = boost::target(e1, g);

    std::ostringstream dot;
    if constexpr (std::is_same<boost::vecS, containerS>()) {
        boost::write_graphviz(dot, g);
    } else {
        std::map<V, int> index;
        for (auto v : boost::make_iterator_range(vertices(g)))
            index.emplace(v, index.size());

        auto index_map = boost::make_assoc_property_map(index);

        boost::dynamic_properties dp;
        dp.property("node_id", index_map); // get(boost::vertex_index, g)

        boost::write_graphviz_dp(dot, g, dp);
    }

    std::cout << "dot: " << replace_all_copy(dot.str(), "\n", "") << "\n";
}

int main() {
    tests<boost::vecS>();
    tests<boost::setS>();
    tests<boost::listS>();
}

Prints

tests() with boost::vecS - vertex_descriptor integral? true
dot: digraph G {0;1;0->1 ;}

tests() with boost::setS - vertex_descriptor integral? false
dot: digraph G {0;1;0->1 ;}

tests() with boost::listS - vertex_descriptor integral? false
dot: digraph G {0;1;0->1 ;}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • About the fact that you usually don't hold references on bundled attributes: sure, I understand that, but here my nodes hold a vector of potentially hundreds elements, so I guessed that passing by value would not be very efficient. Also, thanks for the clarification on what you meant by "integral arguments" (node Id). I'm still into it. – kebs Feb 22 '21 at 08:09
  • Ok, I got throught your code, thanks for your time (again!). What is really nice in your answers is that you don't just give the solution but detail all the different considerations, and go beyond what the OP asked for. I always learn a lot by reading them. Side note: I realize I'll probably need to get into dynamic properties for BGL. At present, I tried to avoid them but there seems to be some situations where you can't. – kebs Feb 22 '21 at 08:39
  • Side question: am I correct by concluding that the string argument in `dp.property()` is just an identification string (in case you need to access it later), and useless here? – kebs Feb 22 '21 at 08:41
  • Yes it's just an identification, and no it isn't useless. The `"node_id"` is special as it is used as the... node ID. All other values are special because they're interpreted as graphviz node/edge attributes. – sehe Feb 22 '21 at 11:40
  • _"but here my nodes hold a vector of potentially hundreds elements, so I guessed that passing by value would not be very efficient"_ - The opposite of "holding a reference" is not "copying everything". It is just literally "not holding a reference" (but holding a stable identification instead). – sehe Feb 22 '21 at 11:42
  • Also, as far as I'm aware few algorithms consume dynamic properties. I think it's merely the generic IO facilities (graphml/graphviz AFAIR). The library interfaces revolves around property maps, not dynamic properties (that would refute the design goals of property map in the first place). Dynamic properties merely connect to that interface "from the other end" of the design spectrum: when nothing is statically known and the code must be able to generate them – sehe Feb 22 '21 at 11:44
  • I'm not sure to understand what you mean by _"not holding a reference" (but holding a stable identification instead)_... I have `struct MyVertex{ vector vec;}`, and new vertices get assigned a subset of that one, that is build from `const auto& vecref = graph[v].vec;`. I understood that adding new vertices invalidates the vertex descriptor `v`. Thus my switch to `ListS`. Did I misunderstand what you suggest? – kebs Feb 22 '21 at 13:23
  • Yeah. Some operations *can* invalidate descriptors, however, in the **vast** majority of operations, descriptors are stable. E.g. pre-size the graph to sufficient size and there's not going to be invalidation (of neither descriptor, iterator nor reference actually) on addition. – sehe Feb 22 '21 at 13:25
  • Indeed, never ran into that issue, as I said I always have used `VecS` in the past. Here, I can't pre-size the graph, it is build recursively. – kebs Feb 22 '21 at 13:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/229041/discussion-between-sehe-and-kebs). – sehe Feb 22 '21 at 13:42