Question

I am developing data structure able to describe semantics of some XML files in C++. Idea is to check presence and/or proper sequence of various elements while storing text they contain into QHash object with QString id's (based on element names, but customizable) as keys.

As XML supports nesting I would like to be able to mirror this nesting. So each XML element is described either by "name" and (optional) "id", what means it is the final leaf and it's text will get parsed, or "name" and list of other element descriptors, what means there should be these nested elements inside the current one.

As there will be plenty of such semantic schemes, I would like to have the code describing their individual instances really compact.

My idea was to have an class, that describes one element and can be constructed via c++ std::initializer_list literals, what I hoped to support nesting implicitly. Various overloaded constructors than can set various specific details later on.

I got almost there, but now got stuck. Even though constructor with signature constructor(std::initializer_list<ProtocolDescriptorTestNode >);
gets called normally for every nested curly braces, simillar signature constructor constructor(QString, std::initializer_list<ProtocolDescriptorTestNode >);
gets never called, even if I place constructs like
{ "xyz", { {"abc", "123"}, {"def","456"} } }
as the initializer literal.

Please take a look at the following code snippets, separated from the testing code and help me to understand if:
1. This is a normal c++11 behavior and std::initializer_list does not support such nesting combined with other data type parameters.
2. This is the matter of implementation (bug ;) ) in gcc (I am using version 4.9.1 (Debian 4.9.1-1))
3. I am overlooking some really stupid detail in code syntax/semantics.

Class declaration (excerpt of relevant constructors):

class ProtocolDescriptorTestNode {

public:
    ProtocolDescriptorTestNode(const ProtocolDescriptorTestNode&);

    ProtocolDescriptorTestNode(std::initializer_list<ProtocolDescriptorTestNode > init); // 1
    ProtocolDescriptorTestNode(QString name, std::initializer_list<ProtocolDescriptorTestNode > init); // 2
    ProtocolDescriptorTestNode(QString name, QString id, enum eElementMode = modeDefault); // 4
    ProtocolDescriptorTestNode(QString name, enum eElementMode = modeDefault); //5

    ~ProtocolDescriptorTestNode() {}

     QString name, id;
     tProtocolDescriptorTestList list;
};

Definitions of relevant constructors:

ProtocolDescriptorTestNode::ProtocolDescriptorTestNode(std::initializer_list<ProtocolDescriptorTestNode> init)
{
    qDebug() << "*** CONSTRUCTOR CALLED - 1 *** ";
    qDebug() << init.size();
    for(ProtocolDescriptorTestNode x : init) {
        qDebug() << x.name << x.id;
    }
}

ProtocolDescriptorTestNode::ProtocolDescriptorTestNode(QString name, std::initializer_list<ProtocolDescriptorTestNode> init) {
    qDebug() << "*** CONSTRUCTOR CALLED - 2 *** ";
    qDebug() << init.size();
    for(ProtocolDescriptorTestNode x : init) {
        qDebug() << x.name << x.id;
    }
}

ProtocolDescriptorTestNode::ProtocolDescriptorTestNode(QString name, QString id, enum eElementMode) :
    name(name),
    id(id)
{
    qDebug() << "*** CONSTRUCTOR CALLED - 4 *** ";
    qDebug() << name << id;
}

ProtocolDescriptorTestNode::ProtocolDescriptorTestNode(QString name, enum eElementMode)  :
    name(name),
    id("***")
{
    qDebug() << "*** CONSTRUCTOR CALLED - 5 *** ";
    qDebug() << name << id;
}

Testing object instance: (notice: implicit/explicit datatype conversion char * / QString does not make difference)

ProtocolDescriptorTestNode groupOther
({
     {QString("name1"),"groupOther1"},
     {"name2","groupOther2"},

     { QString("B"), {
        {"name3","groupOther3"},
        {
             {"intra1","a"},
             {QString("intra2")}
         },
        {"name4","groupOther4"}
     } }

 });

And relevant part of debug output, showing that the part of the initialization near the "B" literal is treated as node(QString("B") and node(std::initializer_list) concatenated, not node(QString("B"), std::initializer_list) as was my intention:

*** CONSTRUCTOR CALLED - 4 *** 
"name1" "groupOther1"
*** CONSTRUCTOR CALLED - 4 *** 
"name2" "groupOther2"
*** CONSTRUCTOR CALLED - 5 *** 
"B" "***"
*** CONSTRUCTOR CALLED - 4 *** 
"name3" "groupOther3"
*** CONSTRUCTOR CALLED - 4 *** 
"intra1" "a"
*** CONSTRUCTOR CALLED - 5 *** 
"intra2" "***"
*** CONSTRUCTOR CALLED - 1 *** 
1
"intra2" "***"
*** CONSTRUCTOR CALLED - 1 *** 
2
"intra1" "a"
"" ""
*** CONSTRUCTOR CALLED - 4 *** 
"name4" "groupOther4"
*** CONSTRUCTOR CALLED - 1 *** 
3
"name3" "groupOther3"
"" ""
"name4" "groupOther4"
*** CONSTRUCTOR CALLED - 1 *** 
2
"B" "***"
"" ""
*** CONSTRUCTOR CALLED - 1 *** 
3
"name1" "groupOther1"
"name2" "groupOther2"
"" ""
Was it helpful?

Solution

The problem you're running into is that if a type has a constructor with a single parameter of type initializer_list (or has additional parameters, but the remaining have default arguments), list initialization will always prefer that constructor over other constructors (§13.3.1.7/1 [over.match.list]). With that in mind, let's work our way through your initialization.

At the outermost level you have a braced-init-list that contains 3 elements:

{
    {QString("name1"),"groupOther1"}  // element 1
    {"name2","groupOther2"}           // element 2
    { QString("B"), ... }             // element 3
}

ProtocolDescriptorTestNode has a constructor that takes a single argument of type initializer_list<ProtocolDescriptorTestNode>. Because of the rule I mentioned earlier, the compiler is going to try to convert each of these 3 elements to a ProtocolDescriptorTestNode.

Each of these elements are themselves braced-init-lists, so an attempt will be made to match the initializer_list constructor first.


Consider the first element:

{QString("name1"),"groupOther1"}

To convert this to an initializer_list<ProtocolDescriptorTestNode>, the second argument would require 2 user defined conversions, first to QString and then to ProtocolDescriptorTestNode, which is not allowed. So other constructors of ProtocolDescriptorTestNode are considered, and it matches constructor 4.

Note that behavior would be different if the first element had been

 {QString("name1"),QString("groupOther1")}

In this case, each element of the braced-init-list would match constructor 5 to create a ProtocolDescriptorTestNode instance, and the two would then form an initializer_list<ProtocolDescriptorTestNode> and match constructor 1.


The next element is

{"name2","groupOther2"}

which again matches constructor 4 for the same reason as the last case.


The third element is

 { QString("B"), 
    {
       {"name3","groupOther3"},  // constructor 4
       {
          {"intra1","a"},        // constructor 4
          {QString("intra2")}    // constructor 1
       },                        // constructor 1
       {"name4","groupOther4"}   // constructor 4
    }                            // constructor 1
 }                               // constructor 1

The first sub-element is implicitly convertible to ProtocolDescriptorTestNode (matching constructor 5), and the second sub-element, itself a braced-init-list, is also convertible to ProtocolDescriptorTestNode (matching constructor 1), because each of the sub-elements within this braced-init-list are themselves implicitly convertible to ProtocolDescriptorTestNode by matching various constructors, as indicated by the comments above.

Thus constructor 2 is never matched.


After that long winded explanation, the fix to your problem is surprisingly simple, and hinted at in the explanation for the first element. The reason element 3 matches the initializer_list constructor is because both its sub-elements are implicitly convertible to ProtocolDescriptorTestNode. So, replace QString("B") with "B". Now its conversion to ProtocolDescriptorTestNode requires two user defined conversions, and the initializer_list constructor is no longer viable. Other constructors will be considered, and constructor 2 will be matched.

Live demo

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top