J. Willus
J. Willus

Reputation: 577

Performing initialization of templated class using other templated classes in variadic arguments of constructor

I wanted to create a simple HTML dom builder in C++ and decided I would use a templated tag<> class to describe the type of tag this was.

I already used other methods to create the DOM in C++ with some success, but the design wouldn't handle raw strings, so the move to a templated class may assist me in handling that using template specialization (tag<plain>).

The issue now is nesting the tags within their constructors using a variadic template. I've been able to implement it with node, which holds the root level tags, but any tag-within-tag nesting is a no-go.

#include <map>
#include <string>
#include <tuple>
#include <utility>

namespace web {
enum class attrs { charset, name, content, http_equiv, rel, href, id, src, lang };

using attribute = std::pair<attrs, std::string>;

using attribute_type = std::map<attrs, std::string>;

const auto none = attribute_type{};

enum tag_name { html, head, meta, title, link, body, div, script, plain, p, h1, span };

template <typename... Tags> struct node {
    int increment;
    std::tuple<Tags...> tags;

    explicit node(const int incr, Tags... tggs)
        : increment{incr}, tags{std::make_tuple(tggs...)} {}
};

template <tag_name T, typename... Tags> struct tag {
    attribute_type attributes;
    std::tuple<Tags...> tags;

    explicit tag(attribute_type atts, Tags... tggs)
        : attributes{atts.begin(), atts.end()}, tags{std::make_tuple(tggs...)} {
    }
};

template <> struct tag<plain> {
    std::string content;

    explicit tag(std::string val) : content{std::move(val)} {}
};
} // namespace web

int main() {
    using namespace web;
    node page1{2};
    node page2{2, tag<html>{none}};
    node page3{2, tag<html>{{{attrs::lang, "en"}}}};
    node page4{2, tag<meta>{{{attrs::name, "viewport"},
                                  {attrs::content,
                                   "width=device-width, initial-scale=1.0"}}}};
    node page5{2, tag<head>{none}, tag<body>{none}, tag<plain>{"Hello World"}}; // Yet this line still compiles and works as expected...
    node page6{1, tag<span>{none, tag<h1>{none}}}; // error: no matching constructor for initialization of 'tag<html>'
}

I want to know how I'm able to aggregate tags inside the node class yet cannot do so inside the tag class and, if possible, I would be able to solve this problem.

Upvotes: 1

Views: 95

Answers (2)

max66
max66

Reputation: 66230

The problem in your code is that deduction guides, introduced in C++17, work only deducing all template arguments.

So calling

node page2{2, tag<html>{none}};

works because

(1) tag<html>{none} doesn't needs template deduction because the first template parameter is explicated where the variadic list (Tags...) is empty (no arguments after none) so the tag is a tag<html> and

(2) automatic deduction guides for node deduce all template arguments (Tags...) so page2 is deduced as node<tag<html>>.

The problem arises when you write

tag<span>{none, tag<h1>{none}}

because, for the tag<span>, there is an argument after none so the variadic list Tags... isn't empty but can't be (automatically, through implicit deduction guides) because you have explicated the first template argument (span).

You can obviously solve the problem adding a make_tag() function, as suggested by Cruz Jean, but I propose you a different solution that uses automatic deduction guides.

First of all, define a wrapper class w for tag_names

template <tag_name>
struct w
 { };

then rewrite your tag class with two constructor; the first one for the case with empty internal tags

  explicit tag (attribute_type atts)
     : attributes{std::move(atts)}
   { }

the second one for the general case (also not empty internal tags list) that receive a w<T> element that permits the automatic deduction also for T

  explicit tag (w<T>, attribute_type atts, Tags... tggs)
     : attributes{std::move(atts)}, tags{tggs...}
  { }

The first constructor permit to maintain the format

 tag<html>{none}

in case of absence of contained tags; the second one permit this type of tag object declarations

 tag{w<html>{}, none}

 tag{w<span>{}, none, tag<h1>{none}}

The following is a full compiling example

#include <map>
#include <string>
#include <tuple>
#include <utility>

namespace web
 {
   enum class attrs
    { charset, name, content, http_equiv, rel, href, id, src, lang };

   using attribute = std::pair<attrs, std::string>;

   using attribute_type = std::map<attrs, std::string>;

   const auto none = attribute_type{};

   enum tag_name
    { html, head, meta, title, link, body, div, script, plain, p, h1, span };

   template <typename... Tags>
   struct node
    {
      int increment;
      std::tuple<Tags...> tags;

      explicit node (int const incr, Tags ... tggs)
         : increment{incr}, tags{tggs...}
       { }
    };

   template <tag_name>
   struct w
    { };

   template <tag_name T, typename ... Tags>
   struct tag
    {
      attribute_type attributes;
      std::tuple<Tags...> tags;

      explicit tag (attribute_type atts)
         : attributes{std::move(atts)}
       { }

      explicit tag (w<T>, attribute_type atts, Tags... tggs)
         : attributes{std::move(atts)}, tags{tggs...}
      { }
    };

   template <>
   struct tag<plain>
    {
      std::string content;

      explicit tag (std::string val) : content{std::move(val)}
       { }
    };
 } // namespace web


int main ()
 {
   using namespace web;
   node page1{2};
   node page2{2, tag<html>{none}};
   node page3{2, tag<html>{{{attrs::lang, "en"}}}};
   node page4{2, tag<html>{{{attrs::name, "viewport"},
       {attrs::content, "width=device-width, initial-scale=1.0"}}}};
   node page5{2, tag<head>{none}, tag<body>{none},
       tag<plain>{"Hello World"}};
   node page6{1, tag{w<span>{}, none, tag<h1>{none}}};
 }

Upvotes: 1

Cruz Jean
Cruz Jean

Reputation: 2819

This seems to be an issue of template class type deduction. There's an ambiguity that can be cleared up by a simple function wrapper (or by C++17 deduction guides).

Anyhow, here you go (this works in gcc 8.3 in C++17 mode):

#include <map>
#include <string>
#include <tuple>
#include <utility>

namespace web
{
    enum class attrs { charset, name, content, http_equiv, rel, href, id, src, lang };

    using attribute = std::pair<attrs, std::string>;

    using attribute_type = std::map<attrs, std::string>;

    const auto none = attribute_type{};

    enum tag_name { html, head, meta, title, link, body, div, script, plain, p, h1, span };

    template <typename... Tags>
    struct node
    {
        int increment;
        std::tuple<Tags...> tags;

        explicit node(const int incr, Tags... tggs) : increment{incr}, tags{tggs...} {}
    };

    template <tag_name T, typename... Tags>
    struct tag
    {
        attribute_type attributes;
        std::tuple<Tags...> tags;

        explicit tag(const attribute_type &atts, Tags... tggs) : attributes(atts), tags(tggs...) {}
    };

    template <>
    struct tag<plain>
    {
        std::string content;

        explicit tag(std::string val) : content(std::move(val)) {}
    };

    template<typename ...Args>
    auto make_node(int incr, Args &&...args)
    {
        return node<std::decay_t<Args>...> ( incr, std::forward<Args>(args)... );
    }
    template<tag_name T, typename ...Args>
    auto make_tag(const attribute_type &atts, Args &&...args)
    {
        return tag<T, std::decay_t<Args>...> ( atts, std::forward<Args>(args)... );
    }
} // namespace web



int main() {
    using namespace web;
    node page1{2};
    node page2{2, tag<html>{none}};
    node page3{2, tag<html>{{{attrs::lang, "en"}}}};
    node page4{2, tag<meta>{{{attrs::name, "viewport"},
                                  {attrs::content,
                                   "width=device-width, initial-scale=1.0"}}}};
    node page5{2, tag<head>{none}, tag<body>{none}, tag<plain>{"Hello World"}};
    auto page6 = make_node(1, make_tag<span>(none, make_tag<h1>(none))); // works now - uses our make functions
}

Upvotes: 2

Related Questions