Piciu Piciu - 4 months ago 11
C++ Question

Type hierarchy based on string, with compile type check

I have a tree hierarchy of types, each defined by string, something like this:

com
com.example
com.example.shape
com.example.shape.triangle
com.example.shape.triangle.equilateral
com.example.shape.triangle.isosceles
com.example.shape.triangle.right
com.example.shape.quadrilateral
com.example.shape.quadrilateral.rectangle
com.example.shape.quadrilateral.squere


Types defines some data with dynamic parameters, that can be changed in runtime, so there is no way to create a compile time type hierarchy. So every entity is just a type name (string) and a list of parameters, and you can always register a new type in the system. Nevertheless a lot of types are predefined, and can be registered when system starts. In order to have the same experience with data created at runtime and predefined, I use this dynamic representation for both. For predefined types I would like to have a mechanism that can validate type name at compile time and I don't want to put strings directly in the code every time it must be used, it can be solved by defining string const expressions, but it is not very nice, something like this:

string some_type = "com.example.type1";
...
registerType(some_type, parameters_definition);


So I am thinking of a better way.

Another approach is to make something like this:

#include <iostream>
#include <string>

struct Base {
Base(std::string parent_name, std::string my_name) : name_(parent_name + "." + my_name) {}
std::string name_;
};

std::ostream& operator<< (std::ostream& os, const Base& base) {
os << base.name_;
return os;
}

struct G : Base {
G(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
};

struct F : Base {
F(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
G rectangle{name_, "rectangle"};
G squere{name_, "squere"};
};

struct E : Base {
E(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
};

struct D : Base{
D(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
E equilateral{name_, "equilateral"};
E isosceles{name_, "isosceles"};
E right{name_, "right"};
};

struct C : Base {
C(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
D triangle{name_, "triangle"};
F quadrilateral{name_, "quadrilateral"};
};

struct B : Base{
B(std::string parent_name, std::string my_name) : Base(parent_name, my_name) {}
C shape{name_, "shape"};
};

struct A {
A(std::string my_name) : name_(my_name) {};
std::string name_;
B example{name_, "example"};
};

std::ostream& operator<< (std::ostream& os, const A& a) {
os << a.name_;
return os;
}


int main() {

A com("com");

std::cout << com << std::endl;
std::cout << com.example << std::endl;
std::cout << com.example.shape << std::endl;
std::cout << com.example.shape.triangle << std::endl;
std::cout << com.example.shape.triangle.equilateral << std::endl;
std::cout << com.example.shape.triangle.isosceles << std::endl;
std::cout << com.example.shape.triangle.right << std::endl;
std::cout << com.example.shape.quadrilateral << std::endl;
std::cout << com.example.shape.quadrilateral.rectangle << std::endl;
std::cout << com.example.shape.quadrilateral.squere << std::endl;

return 0;
}


It is nice to use, especially having IDE with code hints, but unfortunately not easy do define. Every different tree level require new class to be defined with new members, that name corresponds with some strings.

I am looking for a better solution - simpler. It would be great to have it defined like some kind of template specialization, but I don't know how to do it.

Any suggestions welcome :)
Regards,
Piciu.

Answer

This is a slightly different approach.

Here we create variable name_tokens matching your com names. We then sticth them together with a smart operator/.

There is a bit of boilerplate first, but the actual grammar is concise:

template<class...Ts>
struct or_trait : std::false_type {};
template<class T0, class...Ts>
struct or_trait<T0, Ts...> : std::integral_constant<bool, T0{} || or_trait<Ts...>{} > {};

namespace names {
  template<bool is_root, class...Parents>
  struct name_token {
    std::string name;
    name_token( std::string in ):name(std::move(in)) {}

    template<class Parent>
    constexpr static bool is_valid() {
      return or_trait< std::is_same<Parents, Parent>... >{};
    }
  };

  template<class...Parents>
  name_token<false, Parents...> name( std::string s, Parents const&... ) { return std::move(s); }

  template<class...Parents>
  struct name_token<true, Parents...> {
    std::string name;
    name_token( std::string in ):name(std::move(in)) {}

    operator std::string() const { return name; }

    template<class Parent>
    constexpr static bool is_valid() {
      return or_trait< std::is_same<Parents, Parent>... >{};
    }
    friend std::ostream& operator<<(std::ostream& os, name_token const& self) {
      return os << std::string(self);
    }
  };

  template<class...Parents>
  name_token<true, Parents...> rootname( std::string s, Parents const&... ) { return std::move(s); }


  template<class LastToken>
  struct name_expression {
    std::string current;
    operator std::string()const { return current; }
    operator std::string()&& { return std::move(current); }

    friend std::ostream& operator<<(std::ostream& os, name_expression const& self)
    {
      return os << std::string(self);
    }
    friend std::ostream& operator<<(std::ostream& os, name_expression && self)
    {
      return os << std::string(std::move(self));
    }
  };

  template<class Lhs, class Rhs,
    std::enable_if_t< Rhs::template is_valid<Lhs>(), int>* =nullptr
  >
  auto operator/( Lhs lhs, Rhs rhs ) {
    return name_expression<Rhs>{ std::string(lhs) + "." + rhs.name };
  }

  template<class Lhs, class Rhs,
    std::enable_if_t< Rhs::template is_valid<Lhs>(), int>* =nullptr
  >
  auto operator/( name_expression<Lhs>&& lhs, Rhs rhs ) {
    return name_expression<Rhs>{
      std::move(
        std::string(std::move(lhs)) += "." + rhs.name
      )
    };
  }

  template<bool b, class...Parents>
  auto operator->*( name_token<b,Parents...> const& parent, std::string n ) {
    return name( std::move(n), parent );
  }
}

We can then create the name tokens as follows:

auto com = names::rootname("com");
auto example = com->*"example";
auto shape = example->*"shape";

auto triangle = shape->*"triangle";
auto right = triangle->*"right";
auto isosceles = triangle->*"isosceles";
auto equilateral = triangle->*"equilateral";

auto quadrilateral = shape->*"quadrilateral";
auto rectangle = quadrilateral->*"rectangle";
auto square = quadrilateral->*"square";

Now com/example works, but com/shape generates a compile-time error.

Live example.