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