1

What is a good way to implement a wrapper class for CRTP lazy evaluation?

Specifically, I am looking to work with delayed or lazy evaluation similar to the answers to this question and am interested in wrapping the CRTP derived classes in a generic class so that I can do something like the following:

Wrapper a = 1;      // Some type like Number<int>
Wrapper b = 2;      // Some type like Number<int>
Wrapper c = a + b;  // An expression like Add<Number<int>, Number<int>>

I think that a smart pointer wrapper makes sense, because I can use an abstract base class for the CRTP base and manage the underlying objects. So far, I've implemented an abstract Base class that has enable_shared_from_this so that I can create a shared_ptr, and a CRTPBase class that handles templated expressions and types.

class Base : public std::enable_shared_from_this<Base> {
public:
  virtual ~Base() {}
  // ... other virtual methods.

  // when we don't have the type.
  inline std::shared_ptr<Base> as_ptr() {
    return shared_from_this();
  }
};

template<typename Derived>
class CRTPBase : public Base {
public:
  // ... overridden virtual methods.

  inline Derived &derived() { 
    return static_cast<Derived &>(*this); 
  }

  inline const Derived &derived() const { 
    return static_cast<const Derived &>(*this); 
  }

  // ... other CRTP stuff.
};

Then, the derived expressions are returned as something like:

template<typename T1, typename T2>
class Add : public CRTPBase<Add<T1, T2>> {
private:
  const T1 &lhs_;
  const T2 &rhs_;

public:
  Add(const T1 &lhs, const T2 &rhs) : lhs_(lhs), rhs_(rhs) {}
}

template<typename T1, typename T2>
inline const Add<T1, T2> operator+(const CRTPBase<T1> &lhs, const CRTPBase<T2> &rhs) {
  return Add<T1, T2>(lhs.derived(), rhs.derived());
}

I'm looking to have a Wrapper class that can take something like a Number<T>, Matrix<T> or an Add<T1, T2> or whatever derives from CRTPBase.

class Wrapper : public CRTPBase<Wrapper> {
private:
  std::shared_ptr<Base> ptr_;

public:
  // rule of zero?

  // example constructor to make a new Number<int>
  explicit inline Wrapper(int value) 
      : ptr_(std::make_shared<Number<int>>(value)) {}

  // what do these look like?
  template<typename T> Wrapper(const CRTPBase<T> &m) { ... }
  template<typename T> Wrapper(CRTPBase<T> &&m) { ... }  
  template<typename T> Wrapper &operator=(const CRTPBase<T> &m) { ... }
  template<typename T> Wrapper &operator=(CRTPBase<T> &&m) { ... }  
};

And because the wrapper also derives from CRTPBase, I can pass it around into expressions just like any other derived type. The goal here is to use the wrapper, and not the actual types.

How can I wrap the CRTP classes returned from expressions in a smart pointer? I'm not very confident with smart pointers and I'm looking for help understanding what it would look like to use them inside this wrapper class.

Currently, I've got things like:

template<typename T> Wrapper(const CRTPBase<T> &&m) 
    : ptr_(std::make_shared<T>(std::move(m.derived()))) {}

Which works (and I don't understand why), but seems wrong.

Adam
  • 303
  • 3
  • 11
  • For `std::make_shared(std::move(m))` to compile, `T` would need a constructor that takes `CRTPBase` It wouldn't have one implicitly - you must have added it explicitly. Show what, say, `Add` class looks like. – Igor Tandetnik Jan 26 '19 at 04:51
  • 1
    What should work is something like `ptr_(std::make_shared(static_cast(m)))`. That'll invoke `T`'s move constructor, which could be implicitly defined. This relies on the assumption that `CRTPBase` doesn't appear by itself, but always as a base class subobject of `T`, so it's safe to downcast. – Igor Tandetnik Jan 26 '19 at 04:56
  • Added more code to make the implementation more clear. – Adam Jan 26 '19 at 04:59
  • Well, you use exactly the `static_cast` I was thinking of - it's just inside `derived()` rather than being spelled out explicitly. Which part do you not understand, and which part seems wrong? – Igor Tandetnik Jan 26 '19 at 05:03
  • Is that calling the move constructor for T? It's strange to me, and I'm reading it slightly differently, wouldn't it be `ptr_(std::make_shared(static_cast(m)))`? – Adam Jan 26 '19 at 05:03
  • You also call `std::move`, which does nothing more than a cast to rvalue reference. – Igor Tandetnik Jan 26 '19 at 05:04
  • Doh. You're right. I guess I'm still wondering about the wrapper implementation for the copy/move constructors. What should be deleted/changed to be safer, etc. There's also a follow-up question about using the wrapper for `a = a + b`, but I wanted to limit it for now. – Adam Jan 26 '19 at 05:10

1 Answers1

0

I'm going to post what worked for me to make the wrapper class work without defaulting to a polymorphic base class.

From an answer to a SO Code Review question I posted here, which suggested using Sean Parent's runtime polymorphism concept, I was able to eliminate the CRTP polymorphic base class Base.

template<typename Derived>
class CRTPBase {
public:
  inline Derived &derived() { 
    return static_cast<Derived &>(*this); 
  }

  inline const Derived &derived() const { 
    return static_cast<const Derived &>(*this); 
  }

  // ... other CRTP stuff.
};

And I made the Add class use copy semantics for safety.

template<typename T1, typename T2>
class Add : public CRTPBase<Add<T1, T2>> {
private:
  const T1 lhs_;
  const T2 rhs_;

public:
  Add(const T1 &lhs, const T2 &rhs) 
      : lhs_(lhs), 
        rhs_(rhs) {}

  // ...
}

Then, a wrapper class can be made that has a type-hiding polymorphic nested class.

class Wrapper : public CRTPBase<Wrapper> {
private:
  struct Concept {
    virtual ~Concept() = default;

    // ... virtual implementation details.
  };

  template<typename T>
  struct Model final : public Concept {
    T data_;

    Model(T data)
        : data_(std::move(data)) {}

    // ... virtual overrides.
  };

  std::shared_ptr<const Concept> ptr_;

public:
  template<typename T>
  inline Wrapper(const CRTPBase<T> &value) 
    : ptr_(std::make_shared<Model<T>>(value.derived())) {}

  // ... functions to interface with ptr_.
};

This allows me to get the semantics I want without forcing the abstract CRTP base class to get carried around. It means I have Add<Wrapper, Wrapper> types, but I can use the static polymorphism in my own code, and the runtime code uses a non-intrusive form of runtime polymorphism as described by Sean Parent.

I still don't know how to evaluate the expressions from the wrappers, mainly because the types held within the Wrapper class are now Concepts or Model<T>s, so I still run into problems during actual evaluation, so if anyone can comment on this, it would be much appreciated :)

Adam
  • 303
  • 3
  • 11