c++nestedc++17initializer-listbraced-init-list

Recursive nested initializer list taking variant


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.


Solution

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