Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Shared pointers. How can I change the underlying object's pointer for all copies?

Tags:

c++

boost

I have a strange situation with virtual classes and I need design help.

  1. The objects do work on destruction,
  2. The objects are stored in a vector, but I need to get the object or a reference to it out of the vector, change it, and have that change reflected in ALL 'copies' of the object,
  3. I'd like the objects to be copyable.

I have a solution, but I am looking for a better one. This is the code I've written that accomplishes what I am looking for, but it depends on pointers to pointers to pointers.

I feel like if I could just directly manipulate the underlying data pointer of the shared pointer, I could get away with one less layer of abstraction.

I would like to do this with one InterfaceWrapper, not two.

#include <stdio.h>
#include <memory>
#include <vector>

class Interface
{
  public:
    virtual void WriteIt() = 0;
    virtual ~Interface() { }
};

class Foo : public Interface
{
  void WriteIt() { printf ("Foo\n"); }
};

class Bar : public Interface
{
  void WriteIt() { printf ("Bar\n"); }
};

// This class wraps Interface so we can call WriteIt on desctruction
// I'd like to do this in the Interface class, but you can't call virtual methods during destruction.
class InterfaceWrapper : public std::unique_ptr<Interface>
{
  public:
  InterfaceWrapper(Interface * i) : std::unique_ptr<Interface>(i) { }
  ~InterfaceWrapper() { (*this)->WriteIt(); }
};

// This class provides counted destruction to InterfaceWrapper
class InterfaceWrapper2
{
  public:
    InterfaceWrapper2 () : _ptr(new InterfaceWrapper(new Foo)) { }

    void MakeBar() { _ptr->reset(new Bar); }

  private:
    std::shared_ptr<InterfaceWrapper> _ptr;
};
    
int main (void)
{
  std::vector<InterfaceWrapper2> thing_vector;

  // The default implementation will, print "Foo" on destruction.
  InterfaceWrapper2 thing;

  // That destructor should only happen once for all copies of 'thing'
  thing_vector.push_back(thing);

  // MakeBar changes the virtual implementation so it prints "Bar" instead of "Foo"
  thing.MakeBar();

  // When destructors happen, the program should just print "Bar" once.

  return 0;
}

Anything is welcome, but I particularly interested in solutions which work on C++03 using boost (my example is C++11 but my 'real' code is C++03 using boost::shared_ptr).

CLARIFICATION

I am basically looking for a better way to implement InterfaceWrapper2 in my example code. main() is the best explanation of what I am trying to accomplish. Just remember the behavior is stuck in those virtual classes.

like image 796
QuestionC Avatar asked Oct 19 '22 21:10

QuestionC


1 Answers

Is this what you're after?

Update 3

If you want to keep the Interface machinery, the following is a pretty succinct way to write the wrapper by just composing standard library features:

class InterfaceWrapper {
    using UI = std::unique_ptr<Interface>;

    std::shared_ptr<UI> _sui {new UI{new Foo}, [](UI*p){ (*p)->WriteIt(); delete p; }};

public:
    void MakeBar() { _sui->reset(new Bar); }
};

See it Live On Coliru

Update 2

  1. After realizing that an std::function<> is already a dynamic, single-function, mutable interface that can be bound to any stateful functor, I thought of the following simplified version:

    Live On Coliru

    #include <memory>
    #include <iostream>
    #include <vector>
    
    struct dynamic_release {
        template <typename F> dynamic_release(F&& f) : _f(std::forward<F>(f)) { }
        template <typename F> dynamic_release& operator=(F&& f) 
            { _f = std::forward<F>(f); return *this; }
    
        ~dynamic_release() { _f(); }
      private:
        std::function<void()> _f;
    };
    
    void do_foo() { std::cout << "Foo\n"; }
    void do_bar() { std::cout << "Bar\n"; }
    
    int main(void) {
        using InterfaceWrapper = std::shared_ptr<dynamic_release>;
        using Thing = InterfaceWrapper::element_type;
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>(do_foo);
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
        } // prints "Foo" once
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>(do_foo);
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
    
            *thing = do_bar; // Prints nothing
    
            thing_vector.push_back(thing);
        } // prints "Bar" once
    }
    
  2. To enable optimizations in case you want to achieve more efficiency with state-less functors too, add a basic_dynamic_release template that allows to use different functor types (like, e.g. void(*)()):

    Live On Coliru

    #include <memory>
    #include <iostream>
    
    namespace detail {
    
        template <typename InterfaceCallable>
        struct basic_dynamic_release {
            basic_dynamic_release() = default;
    
            template <typename F> basic_dynamic_release(F&& f) : _f(std::forward<F>(f)) { }
            template <typename F> basic_dynamic_release& operator=(F&& f) 
                { _f = std::forward<F>(f); return *this; }
    
            ~basic_dynamic_release() { _f(); }
        private:
            InterfaceCallable _f;
        };
    }
    
    using dynamic_release = detail::basic_dynamic_release<std::function<void()>>;
    
    #include <vector>
    
    void do_foo() { std::cout << "Foo\n"; }
    void do_bar() { std::cout << "Bar\n"; }
    
    int main(void) {
        using InterfaceWrapper = std::shared_ptr<detail::basic_dynamic_release<void(*)(void)>>;
        using Thing = InterfaceWrapper::element_type;
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>(do_foo);
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
        } // prints "Foo" once
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>(do_foo);
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
    
            *thing = do_bar; // Prints nothing
    
            thing_vector.push_back(thing);
        } // prints "Bar" once
    }
    
  3. To allow for default constructed instances to have a well defined implementation of the interface, adding a factory (this is to make it very generic):

    Live On Coliru

    #include <memory>
    #include <iostream>
    
    namespace detail {
    
        template <typename T> struct default_construction final {
            T operator()() const { return {}; }
        };
    
        template <typename InterfaceCallable, typename Factory = default_construction<InterfaceCallable> >
        struct basic_dynamic_release {
            basic_dynamic_release() = default;
    
            template <typename F> basic_dynamic_release(F&& f) : _f(std::forward<F>(f)) { }
            template <typename F> basic_dynamic_release& operator=(F&& f) 
                { _f = std::forward<F>(f); return *this; }
    
            ~basic_dynamic_release() { _f(); }
        private:
            InterfaceCallable _f = Factory()();
        };
    
        using dynamic_interface = std::function<void()>;
        template <typename Factory = default_construction<dynamic_interface> >
        using dynamic_release   = basic_dynamic_release<dynamic_interface, Factory>;
    }
    
    #include <vector>
    
    void do_foo() { std::cout << "Foo\n"; }
    void do_bar() { std::cout << "Bar\n"; }
    
    struct foo_default { detail::dynamic_interface operator()() const { return do_foo; } };
    
    int main(void) {
        using InterfaceWrapper = std::shared_ptr<detail::dynamic_release<foo_default> >;
        using Thing = InterfaceWrapper::element_type;
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>();
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
        } // prints "Foo" once
    
        {
            std::vector<InterfaceWrapper> thing_vector;
    
            auto thing = std::make_shared<Thing>();
    
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
            thing_vector.push_back(thing);
    
            *thing = &do_bar; // Prints nothing
    
            thing_vector.push_back(thing);
        } // prints "Bar" once
    }
    

Old answer

The old answer favoured static polymorphism using boost::variant, at the cost of being somewhat more complex the manage, but with greater flexibility:

I opted to replace the dynamic polymorphism with static polymorphism, which removes the extra allocation, which also takes with it the lifetime management (what used to be the unique_ptr).

I think this makes resulting solution a bit simplified, and at the same time more generic (naturally provides some extension points).

Live On Coliru

#include <boost/variant.hpp>
#include <memory>
#include <iostream>

namespace nature { // detail namespace
    template <typename> struct Nature;
    template<> struct Nature<struct FooTag> { void do_it() { std::cout << "Foo" << "\n"; } };
    template<> struct Nature<struct BarTag> { void do_it() { std::cout << "Bar" << "\n"; } };

    using FooNature = Nature<FooTag>;
    using BarNature = Nature<BarTag>;

    using AnyNature = boost::variant<FooNature, BarNature>;

    struct Holder {
        AnyNature held;
        ~Holder() { DoIt()(held); }

      private:
        struct DoIt : boost::static_visitor<> {
            void operator()(AnyNature& any)                  const { return boost::apply_visitor(*this, any); }
            template <typename N> void operator()(N& nature) const { return nature.do_it(); }
        };
    };

}

#include <vector>

int main(void) {
    using InterfaceWrapper = std::shared_ptr<nature::Holder>;
    using Thing = InterfaceWrapper::element_type;

    {
        std::vector<InterfaceWrapper> thing_vector;

        auto thing = std::make_shared<Thing>(); // FooNature is default

        thing_vector.push_back(thing);
        thing_vector.push_back(thing);
        thing_vector.push_back(thing);
        thing_vector.push_back(thing);
    } // prints "Foo" once

    {
        std::vector<InterfaceWrapper> thing_vector;

        auto thing = std::make_shared<Thing>();

        thing_vector.push_back(thing);
        thing_vector.push_back(thing);
        thing_vector.push_back(thing);

        thing->held = nature::BarNature {}; // prints nothing

        thing_vector.push_back(thing);
    } // prints "Bar" once
}

Prints

Foo
Bar
like image 110
sehe Avatar answered Oct 22 '22 12:10

sehe