Recursive nested initializer list taking variant

191 Views Asked by At

I want an object obj to be initialized from an initializer_list of pairs. However, the second value of the pair is a variant of bool, int and again the obj. gcc reports trouble finding the right constructors I guess. Can I make this work recursively for the following code?

#include <utility>
#include <string>
#include <variant>

struct obj;

using val = std::variant<int, bool, obj>;

struct obj
{
    obj(std::initializer_list<std::pair<std::string, val>> init) {
        
    }
};

int main()
{
    obj O = { {"level1_1", true }, { "level1_2", 1 }, { {"level2_1", 2}, {"level2_2", true}}};
}

gcc 12.1 doesn't get it:

<source>: In function 'int main()':
<source>:57:93: error: could not convert '{{"level1_1", true}, {"level1_2", 1}, {{"level2_1", 2}, {"level2_2", true}}}' from '<brace-enclosed initializer list>' to 'obj'
   57 |     obj O = { {"level1_1", true }, { "level1_2", 1 }, { {"level2_1", 2}, {"level2_2", true}}};
      |                                                                                             ^
      |                                                                                             |
      |                     

Any hints on what I need to change?

EDIT: It seems that the approach using initializer lists is probably not suitable. Maybe there exists a solution using templates which is also appreciated. I know it is possible from nlohmanns json library (see section "JSON as first-class data type"). Also the solution should get by without using heap allocations during the initialization process.

2

There are 2 best solutions below

4
mitch_ On BEST ANSWER

As Igor noted, without any changes to your type or constructor, your code can compile fine with an explicit type specified for your subobject:

obj O = { 
  { "level1_1", true },
  { "level1_2", 1    },
  { "level2",  obj {
      { "level2_1", 2    },
      { "level2_2", true },
    }
  },
};

I believe the problem may originate with the particular combination of std::pair, along with a variant that contains an incomplete type. We can observe this by replacing std::pair<...> with our own custom type that lacks any internal storage:

struct obj
{
  using mapped_type = std::variant<int, bool, obj>;

  struct kv_pair
  {
    kv_pair(const std::string &k, int  v) { }
    kv_pair(const std::string &k, bool v) { }
    kv_pair(const std::string &k, const obj &v) { }
  };
  
  obj(std::initializer_list<kv_pair> entries) { }
};

int main(void)
{
  obj _ = {
    { "level1_1", true },
    { "level1_2", 1    },
    { "level2", {
        { "level2_1", 2     },
        { "level2_2", false },
      },
    },
  };
  return 0;
}

With this, we get your desired syntax, but lack any implementation. My guess here is that the problem lies with std::pair in combination with std::variant. We can get back our storage by having kv_pair instead be a convenience wrapper over pairs of strings and pointers, i.e.

struct kv_pair : public std::pair<std::string, std::shared_ptr<mapped_type>>
{
  kv_pair(const std::string &k, int        v) : pair(k, std::make_shared<mapped_type>(v) { }
  kv_pair(const std::string &k, bool       v) : pair(k, std::make_shared<mapped_type>(v) { }
  kv_pair(const std::string &k, const obj &v) : pair(k, std::make_shared<mapped_type>(v) { }
};

So for a full and complete implementation:

struct obj
{
  using mapped_type = std::variant<int, bool, obj>;

  struct kv_pair : public std::pair<std::string, std::shared_ptr<mapped_type>>
  {
    kv_pair(const std::string &k, int        v) : pair(k, std::make_shared<mapped_type>(v)) { }
    kv_pair(const std::string &k, bool       v) : pair(k, std::make_shared<mapped_type>(v)) { }
    kv_pair(const std::string &k, const obj &v) : pair(k, std::make_shared<mapped_type>(v)) { }
  };

  obj(std::initializer_list<kv_pair> entries) 
  {
    for (auto &[k, v]: entries)
    {
      _entries.emplace(k, *v); 
    }
  }

protected:
  std::map<std::string, mapped_type> _entries;
};

int main(void)
{
  obj _ = {
    { "level1_1", true },
    { "level1_2", 1    },
    { "level2", {
        { "level2_1", 2     },
        { "level2_2", false },
      },
    },
  };
  return 0;
}
0
rturrado On

You could use template metaprogramming to create specific types at compile time.

template <int Level>
struct obj {
    using value =
        std::initializer_list<
            std::pair<
                std::string_view,
                std::variant<bool, int, typename obj<Level-1>::value>>>;
};

template <>
struct obj<1> {
    using value
        = std::initializer_list<
            std::pair<
                std::string_view,
                std::variant<bool, int>>>;
};

int main() {
    obj<2>::value o{
        { "level1_1", true },  // (string, bool)
        { "level1_2", 1 },  // (string, int)
        { "level2", obj<1>::value{  // (string, obj)
                { "level2_1", 2 },
                { "level2_2", true }}}};
}

In the code above, obj<2>::value would be compiled to:

std::initializer_list<  // obj<2>::value
    std::pair<
        std::string_view,
        std::variant<
            bool,
            int,
            std::initializer_list<  // obj<1>::value
                std::pair<
                    std::string_view,
                    std::variant<bool, int>>>>>>

I believe no heap allocations should be made, since you are only making use of basic types (bool, int), std::string_view, std::pair and std::variant (should behave like a struct), and std:initializer_list. Check here for a demo showing all the o values in the example above are on the stack.