Detecting null pointers inside vector of boos::variant

Viewed 116

Following the question in Heterogenous vectors of pointers. How to call functions.

I would like to know how to identify null points inside the vector of boost::variant.

Example code:

#include <boost/variant.hpp>
#include <vector>

template< typename T>
class A
{
    public:
        A(){}
        ~A(){}

        void write();

    private:
        T data;
};

template< typename T>
void A<T>::write()
{
    std::cout << data << std::endl;
}

class myVisitor
: public boost::static_visitor<>
{
    public:
        template< typename T>
        void operator() (A<T>* a) const
        {
            a->write();
        }
};

int main()
{
   A<int> one;
   A<double> two;

   typedef boost::variant<A<int>*, A<double>* > registry;
   std::vector<registry> v;

   v.push_back(&one);
   v.push_back(&two);

   A<int>* tst = new A<int>;

   for(auto x: v)
   {
       boost::apply_visitor(myVisitor(), x);
       try {delete tst; tst = nullptr;}
       catch (...){}
   }

}

Since I am deleting the pointer I would hope that the last one will give me an error or something. How can I check if the entry in the entry is pointing to nullptr?

1 Answers

Note: this partly ignores the X/Y of this question, based on the tandom question (Heterogenous vectors of pointers. How to call functions)

What you seem to be after is polymorphic collections, but not with a virtual type hierarchy.

This is known as type erasure, and Boost Type Erasure is conveniently wrapped for exactly this use case with Boost PolyCollection.

The type erased variation would probably look like any_collection:

Live On Coliru

#include <boost/variant.hpp>
#include <cmath>
#include <iostream>
#include <vector>

#include <boost/poly_collection/any_collection.hpp>
#include <boost/type_erasure/member.hpp>
namespace pc = boost::poly_collection;

BOOST_TYPE_ERASURE_MEMBER(has_write, write)
using writable = has_write<void()>;

template <typename T> class A {
  public:
    A(T value = 0) : data(value) {}
    // A() = default; // rule of zero
    //~A() = default;

    void write() const { std::cout << data << std::endl; }

  private:
    T data/* = 0*/;
};

int main()
{
    pc::any_collection<writable> registry;
    A<int>    one(314);
    A<double> two(M_PI);

    registry.insert(one);
    registry.insert(two);

    for (auto& w : registry) {
        w.write();
    }
}

Prints

3.14159
314

Note that the insertion order is preserved, but iteration is done type-by-type. This is also what makes PolyCollection much more efficient than "regular" containers that do not optimize allocation sizes or use pointers.

BONUS: Natural printing operator<<

Using classical dynamic polymorphism, this would not work without adding virtual methods, but with Boost TypeErasure ostreamable is a ready-made concept:

Live On Coliru

#include <boost/variant.hpp>
#include <cmath>
#include <iostream>
#include <vector>

#include <boost/poly_collection/any_collection.hpp>
#include <boost/type_erasure/operators.hpp>
namespace pc = boost::poly_collection;

using writable = boost::type_erasure::ostreamable<>;

template <typename T> class A {
  public:
    A(T value = 0) : data(value) {}
    // A() = default; // rule of zero
    //~A() = default;

  private:
    friend std::ostream& operator<<(std::ostream& os, A const& a) {
        return os << a.data;
    }

    T data/* = 0*/;
};

int main()
{
    pc::any_collection<writable> registry;
    A<int>    one(314);
    A<double> two(M_PI);

    registry.insert(one);
    registry.insert(two);

    for (auto& w : registry) {
        std::cout << w << "\n";
    }
}

Printing the same as before.

UPDATE

To the comment:

I want to create n A<someType> variables (these are big objects). All of these variables have a write function to write something to a file.

My idea is to collect all the pointers of these variables and at the end loop through the vector to call each write function. Now, it might happen that I want to allocate memory and delete a A<someType> variable. If this happens it should not execute the write function.

This sounds like one of the rare occasions where shared_ptr makes sense, because it allows you to observe the object's lifetime using weak_ptr.

Object Graph Imagined...

Let's invent a node type that can participate in a pretty large object graph, such that you would keep an "index" of pointers to some of its nodes. For this demonstration, I'll make it a tree-structured graph, and we're going to keep References to the leaf nodes:

using Object    = std::shared_ptr<struct INode>;
using Reference = std::weak_ptr<struct INode>;

Now, lets add identification to the Node base so we have an arbitrary way to identify nodes to delete (e.g. all nodes with odd ids). In addition, any node can have child nodes, so let's put that in the base node as well:

struct INode {
    virtual void write(std::ostream& os) const = 0;
    std::vector<Object> children;

    size_t id() const { return _id; }
  private:
    size_t _id = s_idgen++;
};

Now we need some concrete derived node types:

template <typename> struct Node : INode {
    void write(std::ostream& os) const override;
};

using Root    = Node<struct root_tag>;
using Banana  = Node<struct banana_tag>;
using Pear    = Node<struct pear_tag>;
using Bicycle = Node<struct bicycle_tag>;
// etc

Yeah. Imagination is not my strong suit ¯\(ツ)

Generate Random Data

// generating demo data
#include <random>
#include <functional>
#include <array>
static std::mt19937                          s_prng{std::random_device{}()};
static std::uniform_int_distribution<size_t> s_num_children(0, 3);

Object generate_object_graph(Object node, unsigned max_depth = 10) {
    std::array<std::function<Object()>, 3> factories = {
        [] { return std::make_shared<Banana>(); },
        [] { return std::make_shared<Pear>(); },
        [] { return std::make_shared<Bicycle>(); },
    };

    for(auto n = s_num_children(s_prng); max_depth && n--;) {
        auto pick  = factories.at(s_prng() % factories.size());
        node->children.push_back(generate_object_graph(pick(), max_depth - 1));
    }

    return node;
}

Nothing fancy. Just a randomly generated tree with a max_depth and random distribution of node types.

write to Pretty-Print

Let's add some logic to display any object graph with indentation:

// for demo output
#include <boost/core/demangle.hpp>

template <typename Tag> void Node<Tag>::write(std::ostream& os) const {
    os << boost::core::demangle(typeid(Tag*).name()) << "(id:" << id() << ") {";
    if (not children.empty()) {
        for (auto& ch : children) {
            ch->write(os << linebreak << "- " << indent);
            os << unindent;
        }

        os << linebreak;
    }
    os << "}";
}

To keep track of the indentation level I'll define these indent/unindent manipulators modifying some custom state inside the stream object:

static auto s_indent = std::ios::xalloc();

std::ostream& indent(std::ostream& os) { return os.iword(s_indent) += 3, os; }
std::ostream& unindent(std::ostream& os) { return os.iword(s_indent) -= 3, os; }
std::ostream& linebreak(std::ostream& os) {
    return os << "\n" << std::setw(os.iword(s_indent)) << "";
}

That should do.

Getting Leaf Nodes

Leaf nodes are the nodes without any children.

This is a depth-first tree visitor taking any output iterator:

template <typename Out>
Out get_leaf_nodes(Object const& tree, Out out) {
    if (tree)  {
        if (tree->children.empty()) {
            *out++ = tree; // that's a leaf node!
        } else {
            for (auto& ch : tree->children) {
                get_leaf_nodes(ch, out);
            }
        }
    }
    return out;
}

Removing some nodes:

Yet another depht-first visitor:

template <typename Pred>
size_t remove_nodes_if(Object tree, Pred predicate)
{
    size_t n = 0;
    if (!tree)
        return n;

    auto& c = tree->children;

    // depth first
    for (auto& child : c)
        n += remove_nodes_if(child, predicate);

    auto e = std::remove_if(begin(c), end(c), predicate);
    n += std::distance(e, end(c));
    c.erase(e, end(c));

    return n;
}

DEMO TIME

Tieing it all together, we can print a randomly generated graph:

int main()
{
    auto root = generate_object_graph(std::make_shared<Root>());
    root->write(std::cout);

This puts all its leaf node References in a container:

    std::list<Reference> leafs;
    get_leaf_nodes(root, back_inserter(leafs));
    

Which we can print using their write() methods:

    std::cout << "\nLeafs: " << leafs.size();
    for (Reference& ref : leafs)
        if (Object alive = ref.lock())
            alive->write(std::cout << " ");
            

Of course all the leafs are still alive. But we can change that! We will remove one in 5 nodes by id:

    auto _2mod5 = [](Object const& node) { return (2 == node->id() % 5); };

    std::cout << "\nRemoved " << remove_nodes_if(root, _2mod5) << " 2mod5 nodes from graph\n";
    std::cout << "\n(Stale?) Leafs: " << leafs.size();
    

The reported number of leafs nodes would still seem the same. That's... not what you wanted. Here's where your question comes in: how do we detect the nodes that were deleted?

    leafs.remove_if(std::mem_fn(&Reference::expired));
    std::cout << "\nLive leafs: " << leafs.size();
    

Now the count will accurately reflect the number of leaf nodes remaining.

Live On Coliru

#include <memory>
#include <vector>
#include <ostream>

using Object    = std::shared_ptr<struct INode>;
using Reference = std::weak_ptr<struct INode>;

static size_t s_idgen = 0;

struct INode {
    virtual void write(std::ostream& os) const = 0;
    std::vector<Object> children;

    size_t id() const { return _id; }
  private:
    size_t _id = s_idgen++;
};

template <typename> struct Node : INode {
    void write(std::ostream& os) const override;
};

using Root    = Node<struct root_tag>;
using Banana  = Node<struct banana_tag>;
using Pear    = Node<struct pear_tag>;
using Bicycle = Node<struct bicycle_tag>;
// etc

// for demo output
#include <boost/core/demangle.hpp>
#include <iostream>
#include <iomanip>

static auto s_indent = std::ios::xalloc();

std::ostream& indent(std::ostream& os) { return os.iword(s_indent) += 3, os; }
std::ostream& unindent(std::ostream& os) { return os.iword(s_indent) -= 3, os; }
std::ostream& linebreak(std::ostream& os) {
    return os << "\n" << std::setw(os.iword(s_indent)) << "";
}

template <typename Tag> void Node<Tag>::write(std::ostream& os) const {
    os << boost::core::demangle(typeid(Tag*).name()) << "(id:" << id() << ") {";
    if (not children.empty()) {
        for (auto& ch : children) {
            ch->write(os << linebreak << "- " << indent);
            os << unindent;
        }

        os << linebreak;
    }
    os << "}";
}

// generating demo data
#include <random>
#include <functional>
#include <array>
static std::mt19937                          s_prng{std::random_device{}()};
static std::uniform_int_distribution<size_t> s_num_children(0, 3);

Object generate_object_graph(Object node, unsigned max_depth = 10) {
    std::array<std::function<Object()>, 3> factories = {
        [] { return std::make_shared<Banana>(); },
        [] { return std::make_shared<Pear>(); },
        [] { return std::make_shared<Bicycle>(); },
    };

    for(auto n = s_num_children(s_prng); max_depth && n--;) {
        auto pick  = factories.at(s_prng() % factories.size());
        node->children.push_back(generate_object_graph(pick(), max_depth - 1));
    }

    return node;
}

template <typename Out>
Out get_leaf_nodes(Object const& tree, Out out) {
    if (tree)  {
        if (tree->children.empty()) {
            *out++ = tree;
        } else {
            for (auto& ch : tree->children) {
                get_leaf_nodes(ch, out);
            }
        }
    }
    return out;
}

template <typename Pred>
size_t remove_nodes_if(Object tree, Pred predicate)
{
    size_t n = 0;
    if (!tree)
        return n;

    auto& c = tree->children;

    // depth first
    for (auto& child : c)
        n += remove_nodes_if(child, predicate);

    auto e = std::remove_if(begin(c), end(c), predicate);
    n += std::distance(e, end(c));
    c.erase(e, end(c));

    return n;
}

#include <list>
int main()
{
    auto root = generate_object_graph(std::make_shared<Root>());
    root->write(std::cout);

    std::list<Reference> leafs;
    get_leaf_nodes(root, back_inserter(leafs));

    std::cout << "\n------------"
              << "\nLeafs: " << leafs.size();
    for (Reference& ref : leafs)
        if (Object alive = ref.lock())
            alive->write(std::cout << " ");

    auto _2mod5 = [](Object const& node) { return (2 == node->id() % 5); };

    std::cout << "\nRemoved " << remove_nodes_if(root, _2mod5) << " 2mod5 nodes from graph\n";
    std::cout << "\n(Stale?) Leafs: " << leafs.size();

    // some of them are not alive, see which are gone ("detecing the null pointers")
    leafs.remove_if(std::mem_fn(&Reference::expired));
    std::cout << "\nLive leafs: " << leafs.size();
}

Prints e.g.

root_tag*(id:0) {
- bicycle_tag*(id:1) {}
- bicycle_tag*(id:2) {
   - pear_tag*(id:3) {}
   }
- bicycle_tag*(id:4) {
   - bicycle_tag*(id:5) {}
   - bicycle_tag*(id:6) {}
   }
}
------------
Leafs: 4 bicycle_tag*(id:1) {} pear_tag*(id:3) {} bicycle_tag*(id:5) {} bicycle_tag*(id:6) {}
Removed 1 2mod5 nodes from graph

(Stale?) Leafs: 4
Live leafs: 3

Or see the COLIRU link for a much larger sample.

Related