0

I am trying to create a compile time polymorphism design that will not require virtual functions with all their drawbacks. However I am struggling with creating simple, effective and easy to understand container that can simulate the ability to hold derived class in it's base class container. My previous attempts with compile time variadic vectors were working, but the code was huge mess. This solutions seems cleaner to me. I have simple code that implements basic CTRP. However I created a runtime container that is storing std::any objects and then based on the type of the object, I can define the action that is supposed to be taken. I have few questions.

  1. How does the usage of std::any and subsequent any_cast<>() hinder the performance compared to the usage of virtual functions?

  2. Is the usage of std::any valid in this situation?

  3. Is there a better way to implement such container?

  4. Is there a way to force implementation as it is with virtual functions (by using virtual <type> foo() = 0)?

  5. Is it a good idea to create an object that will be a CRTP handler? So I will not have a function for CRTP call, but an object, that can manage those calls?

Thank you.

Here is the base class:

class base {
private:
  base() = default;
  friend T;

  T& implementation = static_cast<T&>(*this);
public:
  auto do_stuff() {
    return implementation.do_stuff();
  }
};

Here is the implementation:

#include <iostream>

class implementation_a : public base<implementation_a> {
public:
  auto do_stuff() {
    std::cout << 42 << std::endl;
  }
};

class implementation_b : public base<implementation_b> {
public:
  auto do_stuff() {
    return 420;
  }
};

Here's the container:

#include <vector>
#include <any>

class crtp_vector {
private:
  std::vector<std::any> vec;

public:
  auto begin() {
    return vec.begin();
  }

  auto end() {
    return vec.end();
  }

  auto empty() {
    return vec.empty();
  }

  auto size() {
    return vec.size();
  }

  void clear() {
    vec.clear();
  }

  void push_back(const std::any& val) {
    vec.push_back(val);
  }

  auto emplace_back(const std::any& val) {
    vec.emplace_back(val);
  }
};

Here's the main:

#include "crtp_container.h"

#include <utility>

/* crtp call handler */
template <typename T>
auto crtp_call(T& val) {
  return val.do_stuff();
}

int main() {
  crtp_vector vec;
  implementation_a A;
  implementation_b B;

  vec.push_back(A);
  vec.push_back(B);

  for(auto &member : vec) {
    if(member.type().name() == typeid(implementation_a).name()) {
      crtp_call(std::any_cast<implementation_a&>(member));
    }
    else if(member.type().name() == typeid(implementation_b).name()) {
      std::cout << crtp_call(std::any_cast<implementation_b&>(member)) << std::endl;
    }
    else {
      std::cerr << "no viable type conversion" << std::endl;
    }
  } 

  return 0;
}
  • I used CRTP to implement an implementation detail in an answer. Might be useful to look at for ideas/inspiration: https://stackoverflow.com/a/63743699/4641116 – Eljay Jun 20 '21 at 12:23
  • 3
    Any "drawbacks" in any attempt to reinvent virtual functions and inheritance will have more "drawbacks" than the "drawbacks" of virtual functions. The reason you're struggling is because there's nothing better than virtual functions that does what virtual functions do. If there was something better, than that's how virtual functions would work. – Sam Varshavchik Jun 20 '21 at 12:26
  • Look at the implementation of `std::any` some day; chances are high, it uses inheritance and virtual functions internally. I expect the performance of `std::any` to be at best the same, and likely worse, than straightforward polymorphism. – Igor Tandetnik Jun 20 '21 at 15:07
  • You aren't actually using CRTP. The code shown doesn't rely on `base` in any way. Nothing will change if you drop `base` entirely. Also, your `crtp_vector` is little different from `std::vector>`; you might as well use that. – Igor Tandetnik Jun 20 '21 at 15:22

1 Answers1

2

You make it way too complicated. The code shown doesn't use base in any way; nothing would change if you simply remove it entirely. Even though you keep saying "CRTP", you aren't actually relying on CRTP for anything.

The code doesn't use the ability of std::any to hold any type; it's only used to hold one of a fixed set of types known at compile time. std::variant is better for this.

All told, the example boils down to this:

class implementation_a {
public:
  auto do_stuff() {
    std::cout << 42 << std::endl;
  }
};

class implementation_b {
public:
  auto do_stuff() {
    std::cout << 420 << std::endl;
    return 420;
  }
};

int main() {
  implementation_a A;
  implementation_b B;

  std::vector<std::variant<implementation_a, implementation_b>> vec;
  vec.push_back(A);
  vec.push_back(B);

  for(auto &member : vec) {
      std::visit([](auto& elem) { elem.do_stuff(); }, member);
  } 

  return 0;
}

Demo

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85