Does C++11's decltype make clone unnecessary?
Asked Answered
C

4

11

The clone paradigm is used to make a copy of a derived class without casting down to the base class type. Unfortunately, clone must be implemented in each subclass (or with a mixin with CRTP).

Is there any chance that C++11's decltype makes this unnecessary?

I don't think the code below actually copies original, but simply points a reference to it. When I tried to use new decltype(*original), I get an error: error: new cannot be applied to a reference type.

Is clone still the way to go in C++11? Or is there some new way to use RTTI to copy a derived class object from a base class pointer?

#include <iostream>

struct Base
{
  virtual void print()
  {
    std::cout << "Base" << std::endl;
  }
};

struct Derived : public Base
{
  int val;
  Derived() {val=0;}
  Derived(int val_param): val(val_param) {}
  virtual void print()
  {
    std::cout << "Derived " << val << std::endl;
  }
};

int main() {
  Base * original = new Derived(1);
  original->print();

  // copies by casting down to Base: you need to know the type of *original
  Base * unworking_copy = new Base(*original);
  unworking_copy->print();

  decltype(*original) on_stack = *original;
  on_stack.print();
  return 0;
}
Contentious answered 3/5, 2012 at 2:32 Comment(0)
L
21

decltype is a static construct. Like all C++ typing constructs, it cannot deduce the runtime type of an object. decltype(*original) is just Base&.

Lawsuit answered 3/5, 2012 at 2:43 Comment(5)
This answer is (along with the previous version of my answer) dangerously wrong. decltype(*original) is actually Base&.Underrate
@Underrate Yes, that's why onstack.print() uses Derived::print. But @NicolBolas' point is well-taken: decltype cannot give me runtime type info.Contentious
You can use std::decay to turn Base& into Base.Implacental
@AndrewTomazos-Fathomling: interesting, why would one use std::decay here rather than std::remove_reference ?Whig
std::decay is more general. It is what std::bind/std::function uses to take a parameter type and figure out what the appropriate "decayed" type is to store it in the functor with.Implacental
U
3

decltype (as its name suggests) gives the declared type (static type) of the expression that it is applied to.

decltype(*original) is Base&, so your code will print

Derived 1
Base
Derived 1

but no copy will be made in the third case.

Clone (or some variant of the pattern) is still the way to go in C++11.

Underrate answered 3/5, 2012 at 2:44 Comment(1)
You can use std::decay to turn Base& into Base.Implacental
K
3

decltype cannot and does not recover dynamic object type. It is a purely static construct.

There's no magic way to copy an object. You must call a constructor of its exact final dynamic type somewhere.

Keyway answered 3/5, 2012 at 2:55 Comment(1)
I love your 2 last sentences ! :)Simulcast
H
2

Is clone still the way to go in C++11? Or is there some new way to use RTTI to copy a derived class object from a base class pointer?

In case anybody is interested in a non-invasive cloning, C++11's lambdas seem to provide some new cloning facilities: Thinking about the issue of cloning, I came to admit that the one who manufactured the original instance of an object should also be the one who can help build a replica. Consider the case where all of your objects are manufactured by some Factory. In this case, additionally to the usual create interface, your factory could be equipped with a clone interface, e.g. like so:

#include <iostream>
#include <functional>
#include <memory>
#include <unordered_map>

template <typename BASE>
struct
Factory {
    private: using
    TCloneFn = std::function<std::shared_ptr<BASE>(BASE const * const)>;

    private:
    static std::unordered_map<BASE const*,TCloneFn> cloneFnMap;

    public: template <typename DERIVED_TYPE, typename...TS>
    static std::shared_ptr<BASE>
    create(TS...args) {
        BASE* obj = new DERIVED_TYPE(args...);
        const std::shared_ptr<BASE> pNewObj =
            std::shared_ptr<BASE>(
                obj,
                [&](BASE* p){
                    cloneFnMap.erase(p);
                    delete p;
                }
            );

        cloneFnMap[obj] = [&](BASE const * const orig){
            std::shared_ptr<BASE> pClone = create<DERIVED_TYPE>(std::ref(static_cast<DERIVED_TYPE const &>(*orig)));
            return pClone;
        };
        return pNewObj;
    }

    public: static std::shared_ptr<BASE>
    clone(std::shared_ptr<BASE const> original) {
        return cloneFnMap[original.get()](original.get());
    }
};

template <typename BASE> std::unordered_map<BASE const*,typename Factory<BASE>::TCloneFn> Factory<BASE>::cloneFnMap;

class Base {
    public: virtual ~Base() throw() {}
    public: virtual void whoAmI() const {
        std::cout << "I am Base instance " << this << "\n";
    }
};


class Derived : public Base {
    std::string name;
    public: Derived(std::string name) : name(name) {}
    public: Derived(const Derived&other) : name("copy of "+other.name) {
    }
    private: virtual void whoAmI() const {
        std::cout << "I am Derived instance " << this << " " << name << "\n";
    }
};

int main() {
    std::shared_ptr<Base> a = Factory<Base>::create<Derived>("Original");
    a->whoAmI();
    std::shared_ptr<Base> copy_of_a = Factory<Base>::clone(a);
    copy_of_a->whoAmI();
    std::shared_ptr<Base> copy_of_a_copy = Factory<Base>::clone(copy_of_a);
    copy_of_a_copy->whoAmI();
    return 0;
}

The trick is to remember how the original was constructed in the Factory::create method (by associating a pointer to the object with a lambda that will invoke the copy-constructor). Never throw away old blueprints, you may need them later-on;-)

Frankly, I still prefer the old clone solution with CRTP etc, but this may trigger some new ideas or come otherwise in handy.

The code above is also at http://ideone.com/kIPFt2

EDIT: As wjl commented, the first version of the code would cause cloneFnMapto steadily grow. After thinking it over, I decided to fix the code by introducing a custom deleter to the shared_ptrs being returned such that the cloneFnMap will also get cleaned up. Still, this should just be considered an experimental proof-of-concept code.

Hassanhassell answered 21/2, 2015 at 15:1 Comment(2)
If someone were to actually use this technique, make sure to create the shared_ptr with a custom deleter that removes the pointer from the cloneFnMap when the object is destroyed, otherwise it will grow forever.Brach
sure thing, I just tried to keep things as minimal as possible.Hassanhassell

© 2022 - 2024 — McMap. All rights reserved.