#CRTP, breaking dependency chains

1 messages · Page 1 of 1 (latest)

tawdry badger
#

So I have been running into a problem with CRTP and inheritance, to sketch a rough illustration I have something like this I want to achieve:

template <typename ChildType> class Base {std::vector<ChildType*> childeren;}
template <typename Derived> class Foo : public Base<Foo>{ Derived* func() {return (Derived*)this;} };
class Bar : public Foo<Bar>{};

Hover this doesn't compile as I can't use Foo as the template argument for Base as Foo alone is an incomplete template argument. I could solve this by simply passing in derived, but then this restricts the ChildTypes to much and it would also never allow me to instantiate a stand alone Foo type what is also a requirment I have.

So any ideas on how I can solve this would be much appreciated. ofcource, if you need any more details let me know.

vague fractalBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

tawdry badger
#

The goal I am trying to accomplish is essentially a builder pattern with CRTP with different node types, where I am trying to restrict the types in certain places.

#

for example, I have a root note, with executor childeren (Bar), but those executors can have executable childeren (Foo)

#
    root.add_child<executor_node>("op")
      ->add_child<entity_node>("player", false, true)
      ->set_is_executable();

so the final usage is something like this

#

This all worked very nicely when the Base wasn't templated and only had the fixed baseclass type, however that is fairly limiting in a bunch of cases and would require casting to get the proper type what isn't ideal

vale cloud
#

Foo generally names a template, not a type

tawdry badger
#

yeah, I know that is what I would need to type the template specialization there to get it compiling in this case, that said it would limit it so that Foo couldn't be instantiated by itself

vale cloud
#

the injected class name makes it you don't have to retype all the template parameter and use Foo to denotes the type/current specialization, but that only works when the injected class name exists, in the class scope

#

inheritance specification happens to be outside of that scope

vale cloud
#

Hover this doesn't compile as I can't use Foo as the template argument for Base as Foo alone is an incomplete template argument.
this is kinda weird to say, crtp essentially works by always passing in an incomplete type into the base template

tawdry badger
#

I might be using slightly wrong wording here, it used to be holding that condition and working fine, the issue started when I wanted to change the design slightly

vale cloud
tawdry badger
#

yeah, sorry noticing I am finding it a bit hard to put stuff in words :)

#
  class command_node
  {
  protected:
    std::vector<std::unique_ptr<command_node>> childeren;
    std::int32_t                               node_id = -1;

  public:
    template<typename command_type, typename... arguments>
      requires std::derived_from<command_type, command_node>
    [[maybe_unused]] command_type *add_child(arguments... args)
    {
      auto node     = std::make_unique<command_type>(args...);
      auto node_ptr = node.get();
      childeren.push_back(std::move(node));
      return node_ptr;
    }

  protected:
    void assign_child_ids(std::int32_t &current_id);
    void write_node(dz::packet_writer &writer);

    virtual std::uint8_t get_flags() const                                     = 0;
    virtual void         write_additional_node_data(dz::packet_writer &writer) = 0;
  };

  class root_node : public command_node
  {
    std::int32_t node_count = -1;

  public:
    void                            finalize();
    [[nodiscard]] dz::packet_writer build_commands_packet();

  protected:
    virtual std::uint8_t get_flags() const override { return 0x0; }
    virtual void         write_additional_node_data(dz::packet_writer &writer) override {};
  };

  enum class permission_level : std::uint8_t
  {
    player     = 0,
    moderator  = 1,
    gamemaster = 2,
    admin      = 3,
    owner      = 4
  };

  template <typename Derived>
  class executable_node : public command_node
  {
  protected:
    bool is_executable = false;
    permission_level required_level;

  public:
    Derived *set_is_executable(bool executable = true)
    {
      is_executable = executable;
      return static_cast<Derived*>(this);
    }

    Derived *set_required_level(permission_level level = permission_level::player)
    {
      required_level = level;
      return static_cast<Derived *>(this);
    }

  protected:
    virtual std::uint8_t get_flags() const override { return is_executable ? 0x04 : 0x0; }
  };

  class literal_node : public executable_node<literal_node>
  {
  private:
    std::string name;

  public:
    literal_node(std::string_view name) : name(name) { }

  protected:
    virtual std::uint8_t get_flags() const override { return 0x1 | executable_node::get_flags(); }
    virtual void         write_additional_node_data(dz::packet_writer &writer) override
    {
      writer.write(name);
    };
  };

Here I have a snippet from the orginal code

#

now I wanted to change it slightly, so that instead of the command_node having std::vector<std::unique_ptr<command_node>> childeren; as argument it has some specialiced type of childeren

vale cloud
#

that it does not know the type of in advance?

#

reread a bit, but do you actually want to turn the command_node into a template so it stores children deriving from a specific branch, or things that specialize a specific template, or something?

#

like, what kind of "specialized type of children" are we talking about here

tawdry badger
#

ommand_node into a template so it stores children deriving from a specific branch
this essentially

#

so a root_node has childeren of type executor_node (not pictured here) and executor_nodes have childeren derivign from type executable_node

vale cloud
#

so who introduces the restriction

tawdry badger
#

Sorry for all the confusion, I'm very much in the designing phase of this, combined with my general issues with making my things clear is not helping.

tawdry badger
vale cloud
#

do you wish to retain the common root, or should each specialization of command_node be fine as their seprate hierarchies

tawdry badger
#

The user can't make up arbitrary restrictions, they are essentially hardcoded

#

Well tbh, they don't need a common root, that was just my intial plan as that seemed simplest

vale cloud
vale cloud
#

but in the end, what is whatever vector supposed to be storing

#

or did you mean that "child" in that context weren't child classes, but child nodes of the node type?

#

so that root_node has child node of type executor node so root_node should have a vector of those executor nodes, not a vector of whatever more generic node type

tawdry badger
vale cloud
tawdry badger
#

Not really

#

Uuuuh well I suppose it inherit behavior actually

vale cloud
#

so to summarize my current understanding

you have a bunch of classes/types for various nodes, they are organized in an inheritance hierarchy, and that probably make sense although it's a bit fuzzy, let's say that's fuzzy point 1
it probably makes sense at least to help factor common code, but your description makes it sound like the child types are more specialized versions of the thing they derived from, so it's likely there is an "is-a" relationship

the object instances of those node types have a parent/children relationship, a node stores/owns/keeps track of its children, and you want a "factory" utility/method to add more children to a node. A design constraint is that a node type should only accept children of a "more specialized type", so a type that derives from itself (in the meaning brought up in fuzzy point 1)
considering the logic is similar for every node, it is desirable to factor that add node/factory code

so far so good? @tawdry badger

#

depending on how exactly you define "more specialized type", there's something that heavily bugs me in that description, and to which the answer can heavily influence/dictates what your options are

#

say your hierarchy from base to derived looks like

root_node
executor_node
executable_node

then what's the expected behaviour of

executable_node most_derived;
executor_node& middle = most_derived;
middle.add_child<executor_node>(/*whatever*/);

?

#

should it be allowed or should it not?
because if it's allowed, then the most derived type cannot actually truly have a vector of unique_ptr to its own type/derived; if you allow a most derived type to be manipulated as its intermediate type, and allow the addition of types derived from that intermediate type, then you pretty much have to allow the addition of children of any type in your inheritance hierarchy
it can however make some sense that if you manipulate the node from a reference to a given type in its hierarchy, that from that reference you can only add children that more specialized than whatever reference type you are using
however it can be plenty awkward to do that, because in the most derived type, when you iterate through your children, then you do not know what type they actually have

#

if it isn't allowed, and you truly want to only have children more specialized than the most derived type, then arguably everyone in the hierarchy should know in some way about what the most derived type is, so that children of the "wrong" type cannot be added

#

those are two very different directions to take your design towards, and I guess this is "key fuzzy point"

tawdry badger
tawdry badger
# vale cloud should it be allowed or should it not? because if it's allowed, then the most de...

okay, yeah this makes a lot of sense and I get what you mean 🤔
So to clear up your point a bit, ill add a bit more detail, so say I have a command like /ban target reason
the tree for this would look something like this: root_node (/) -> executor_node (ban) -> entity_node (target) -> message_node (reason)
(both enity_node and message_node are derived classes of executable_node)
executor_node in this case denotes a command that can be executed, in this case the "ban" command, and this then has 1 child that can be of any specialized executable_node and that child (target) has 1 child of any executable_node

vale cloud
#

so executable_node doesn't really derive from executor_node, it's more that executor_node only allow a specific hierarchy of nodes as children

#

but to get back to my example, if you have an executor_node, it doesn't yet have a child, you manipulate it through a reference to root_node, what happens if you try to add a child then?

#

or maybe root_node and executor_node shouldn't be in the same hierarchy?

tawdry badger
#

so right, to get to that, the root node is special in that sense, that it only allows childeren of the executor_node type and any other node type shouldn't be allowed to be added to it

#

thinking about this, I honestly think they realy shouldn't have a shared root node

#

that would propably also solve most problems I have I believe

vale cloud
#

pretty much

#

like, the only polymorphic part that was brought up in the last couple messages, was that you probably want to be able to store any node derived from executable_node as children of executor

#

and similarly types derived from executor as children of root

#

but those look like three distinct hierarchies

#

you can have a "how to store/add child base type", to inject the child handling in the derived type

tawdry badger
#

also already thank you very much for thinking along :)
sometimes simply being able to talk to somone can help a lot

vale cloud
#

and that base would take some information as to what branch/hierarchy of child is allowed

tawdry badger
#

okay, yeah I am liking that 🤔

vale cloud
#

quack (rubber ducking)

tawdry badger
#

nah, it genuinly helped a lot :)
expecially since I have a lot of questions about the entire design as well. (like as in it's not done)

vague fractalBOT
#

@tawdry badger Has your question been resolved? If so, type !solved :)