1

I have an issue similar to this C++ vector of CRTP shared pointers but my problem formulation adds the fact that the return type of a function i want to use for all inheriting classes is templated.

In detail lets assume this :

template <class Derived>
class Base {
    Derived Value() const {
        return static_cast<Derived>(this->Value());
    };
};

class ChildDouble : public Base<ChildDouble> {
  public:
    ChildDouble(double r) : _value(r){};

    double Value() const {
        return _value;
    };

  private:
    double _value;
};

class ChildString : public Base<ChildDouble> {
  public:
    ChildString(string s) : _value(s){};

    string Value() const {
        return _value;
    };

  private:
    string _value;
};

Goal would be to use it somewhat similar as in the following main

void main() {


    std::vector<Base*> vec;
    vec.push_back(new ChildDouble(3.0));
    vec.push_back(new ChildString("Thomas"));

    unsigned counter = 0;
    for (const auto& e : vec) {
        std::cout << "Entry " << counter << " : " << e->Value()
                  << std::endl;
        counter++;
    }

}

The compiler is obviously not happy with this because Base requires a template argument.

Any Ideas how this could be solved? AM I using CRTP here although i should not be using it?

  • You are trying to mix static polymorphism with regular polymorphism. It's not possible to make it work like in your example. You have to choose if you want different return types but also different base classes, or same return type and same base class. – super Dec 30 '19 at 17:16
  • Conceptually, how would your client use the `Base::Value()` function without knowing the specific template argument? All it could tell is that the function returns something. But it does not have any information about what that something is. So how could it use it for anything? parktomatomi shows one option, where the client knows what the return type could possibly be. But it is a rather strong restriction. And the restriction comes from the client, not form the base class. – Nico Schertler Dec 30 '19 at 17:27

2 Answers2

2

Virtual methods (which is what you'd normally need to get the above working without CRTP) won't work here because the interface is different for Value() in each derived type. Virtual inheritance depends on the signature being the same for everyone, except in a few special cases like with covariant return types. It also won't work because virtual methods can't be templated.

But, you can use std::variant to dynamically dispatch your incompatible interfaces, because it is based on templates. First, define a convenient alias for your variant:

using Child = std::variant<ChildDouble, ChildString>;

And then to use, dispatch with std::visit and a generic lambda:

std::vector<Child> vec;
vec.push_back(ChildDouble(3.0));
vec.push_back(ChildString("Thomas"));

unsigned counter = 0;
for (const auto& e : vec) {
    std::visit([&counter](auto&& v) { 
        std::cout << "Entry " << counter << " : " << v.Value()
                << std::endl;
    }, e);

    counter++;
}    

Demo: https://godbolt.org/z/bENWYW

parktomatomi
  • 3,851
  • 1
  • 14
  • 18
  • Using a `variant`, you can get rid of the inheritance altogether: https://godbolt.org/z/JAUnoQ. In fact, in the original example I'm not sure that the `static_cast` was even used correctly. The original says `return static_cast(this->Value());`, but shouldn't it instead have said `return static_cast(this)->Value();`? I'm not quite sure what's even happening when the result of `Value()` is `static_cast`ed to `Derived` as was done in the original example... – Quokka Dec 31 '19 at 19:13
  • 1
    @Quokka Quite true that there is no inheritance requirement for `std::variant`. Also, nice catch on the error in the original example, but the base method _is_ well formed. The result of `Base::Value()` is a `Derived`, so casting it would have no effect. But if the base method is executed, it would cause a stack overflow because it's infinitely recursive – parktomatomi Dec 31 '19 at 19:39
1

It doesn't work because the compiler doesn't know which type you want to put in the vector and you need to specified it. If you try vector<Base<double>*>vec; it will works but you can't use the vector with other types like Base, because, it is other type. The solution is to use std::variant or std::any in place of template.

Now you have an object variant/any the declare value in base will make your life easier.

Also I suggest you:

  • not to use variables starting with underline '_' because this syntax is used by many internal function of compiler.
  • not to use raw pointer. use smart_ptr like share_ptr then you don't need to worry to destroy it with delete.

Below the code with the changes:

#include <memory>
#include <vector>
#include <string>
#include <variant>
#include <iostream>

using namespace std;

struct Base {
       Base(variant<double, string> val) : value(val) {}

    void Print() {  //just to ilustrate how it works. Better use ostream
         if (holds_alternative<double>(this->value))
            cout << get<double>(this->value);
         else if (holds_alternative<string>(this->value))
                 cout << get<string>(this->value);
    }

    protected:
        variant<double, string> value;
        variant<double, string> BaseValue() const { return this->value; };
};

struct ChildDouble : public Base {
       ChildDouble(double r) : Base(r) {};

    double Value() const { return get<double>(this->BaseValue()); }
};

struct ChildString : public Base {
       ChildString(string s) : Base(s) {};

    string Value() const { return get<string>(this->BaseValue()); };
};

int main() {                                //must return int not void
    vector<shared_ptr<Base>>vec;
    vec.emplace_back(new ChildDouble(3.0));
    vec.emplace_back(new ChildString("Thomas"));

    unsigned counter = 0;
    for (const auto& e : vec) {
        cout << "Entry " << counter << " : "; e->Print(); cout << endl;
        ++counter;
    }
}
TheArchitect
  • 1,160
  • 4
  • 15
  • 26