Recursive nested initializer list taking variant

Viewed 75

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 Answers

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.

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;
}
Related