4

Preamble

Before reading this, please be aware I am a C++ beginner. I have not learnt all the (basic) concepts yet (like templates for example), but I am attempting the understand some fundamentals fully before moving on to something else.

So please do not mention std::string or std:array, vectors, boost::arrays, and their superiority to C-style arrays. I am sure their are, but that is not the point here :)

I also now that it is sometimes preferred to assign elements in the ctor body than using its member initializer list.

My Question

Considering the following example. When I reached the moment I had to define a copy constructor to initialize my character2 declared in main(), I noticed, in Visual Studio 2017, that IntelliSense displays 3 different overloads to initialize the class members from the initializer list. For example, with the 'name' variable, the available listed overloads are:

  1. char [50](const char [50] &)
  2. char [50](char [50] &&)
  3. char [50]()

Subsidiary question 1: Why IntelliSense allows to display these overloads only when the variable is declared as class member ? (nothing is shown with CTRL+SHIFT+SPACE if declared in main())


The one that intrigues me most is the one with an rvalue reference in it :

  • char [50](char [50] &&) for the 'name' variable
  • int [10](int [10] &&) for the 'data' variable
  • int(int &&) for the 'singledata' variable

Now, if I use name() in an initializer list, overload #3 appears to be used. If I use name("aaa"), overload #1 appears to be used. This got me to investigate on other concepts: I now understand the concepts of lvalue, rvalue, and lvalue references (but not this, too much complicated for me right now), and I fail to completely understand the concept of rvalue reference and move semantics.

Considering my partial understanding, I tried to declare an rvalue reference in my "parameterized constructor 2" (which then becomes an lvalue of type "rvalue reference to int") and to initialize the 'singledata' variable with it, hoping to see the initializer overload #3 poping up in IntelliSense, but it does not. The one with the lvalue reference parameter (overload #2) appears to be used again in this case. That I can't explain.

So here I am, stuck, and this is my MAIN QUESTION 1: When does the initializer with an rvalue reference parameter happen to be used for a class member?


MAIN QUESTION 2: Why is it not possible to initialize a char array using another char array while it is possible using string literal expressions? because there are pure rvalues (prvalues) while a variable could only be converted to an xvalue expression at best?

I know this is absolutely not standard, but if I look at the dissassembly, the initialization of an array of chars is quite simple:

     2:     const char arrchar1[10]("hello");
01142771 A1 38 9B 14 01       mov         eax,dword ptr [string "hello" (01149B38h)]  
01142776 89 45 D8             mov         dword ptr [arrchar1],eax  
01142779 66 8B 0D 3C 9B 14 01 mov         cx,word ptr ds:[1149B3Ch]  
01142780 66 89 4D DC          mov         word ptr [ebp-24h],cx  
01142784 33 C0                xor         eax,eax  
01142786 89 45 DE             mov         dword ptr [ebp-22h],eax 

the stack-allocated space [arrchar1] (being [ebp-28h] in my case) in the current stack frame is simply filled by a MOV instruction set (2, really, in this case: 1 dword is moved for "hell", then 1 word is moved for the rest) with the contents of the static space containing the "hello" string literal.

Why is there no C++ way resulting in something similar once compiled, which would perform the same thing but 'moving' the contents of a stack-allocated space (another array variable), rather than 'moving' some static memory? I would expect something like this: char arrchar2[10](arrchar1)


Example:

#include <string.h> // for strcpy

class Character
{
public:
    //default constructor
    Character() {};

    //parameterized constructor 1
    Character(const char * pname) : // pointer to the string literal must be const (otherwise it is Undefined Behaviour)
        name(), data{ 1, 2, 3 } // mem-initializer-list: 
                                // - name is initialized with value-initialization (empty expression-list in a pair of parentheses following identifier)
                                // - (C++11) data is initialized using list-initialization which becomes aggregate-initialization since it is an aggregate (array)
    {
        strcpy_s(name, pname);
    };

    //parameterized constructor 2
    Character(const char * pname, int &&val1) :
        name(), data{ 1, 2, 3 }, singledata(val1)
    {
        strcpy_s(name, pname);
    };

    //copy constructor
    Character(const Character & tocopy)
        // member initializer list

        //:name(),  // >> IntelliSense shows 3 initializer overloads: char [50](const char [50] &) || char [50](char [50] &&) || char [50]()
                    // >> (visible only if the array is declared in a class, no pop up in main...)

        : name("aaa"),

        //data()    // >> IntelliSense shows 3 initializer overloads: int [10](const int [10] &) || int [10](int [10] &&) || int [10]()
                    // >> (visible only if the array is declared in a class, no pop up in main...)

        data{ 1, 2, 3 }
    {
        // ctor body definition
    };
private:
    char name[50];
    int data[10];
    int singledata;
};

void main()
{

    Character character1("characterOne"); // the string literal has static storage duration (static memory), passed pointer allocated on stack memory
    Character character2(character1);
    Character character3("characterThree", 3);
}
hymced
  • 570
  • 5
  • 19
  • 3
    Short answer: C style arrays are not like other types. They are not "fundamental", but an exception to the rules. So please use C++ containers to make your job easier (couldn't resist :-). – Bo Persson Dec 07 '17 at 13:24
  • Very interesting. I never thought about [this](https://i.stack.imgur.com/sD5Ti.png) popping up. – wally Dec 07 '17 at 13:46
  • I wouldn't call templates the basics of C++, you can do most of your work in C++ and only ever use templates in the simplest of manners... not write them yourself. e.g. stdlib containers etc. – Mgetz Dec 13 '17 at 14:55
  • Please do not post Answers in the Question. Answers go in the Answer box. You can either accept a posted answer, or post one of your own. – M.M Dec 15 '17 at 14:20
  • Also you should post two separate questions instead of "main question 1" and "main question 2" – M.M Dec 15 '17 at 14:20
  • @M.M k thanks your the advice. In fact the both questions are very related since I came up with the first when investigating the second... but I agree, it would be clearer ! – hymced Dec 15 '17 at 14:52

2 Answers2

1

Question number 1:

I believe you're asking why your singledata initialization is not invoking its r-value initialiser. Short answer: because you're not passing it an r-value.

In your constructor above, the parameter val1 has an r-value reference type. However, remember that the 'r-valueness' property is something that applies to expressions not types. In the initialization singledata(val1), the sub-expression val1 is an l-value, even though the variable val1 has an r-value reference type. It is an l-value as defined by the rules that make something an l-value. The rules, by the way, are literally just a list of conditions that if met make an expression an l-value. Roughly speaking though, you can explain that val1 is an l-value here because its an object whose address can be taken.

Is you wanted the r-value reference version of the initializer for singledata, you would use singledata(std::move(val1)) instead. Here the expression std::move(val1) has an r-value type, by the definition of what std::move does. An r-value reference inside the singledata constructor can thus bind to it.

Smeeheey
  • 9,906
  • 23
  • 39
  • I understand your explanation, and have actually tried the same thing before posting my question : `singledata(std::move(val1))` but in this case, IntelliSense does not display the expected overloard #2 (so I thought not to mentioned it...), it still display the overload #1 : https://i.stack.imgur.com/HTnjY.png -- I am starting to think IntelliSense simply does not work properly : // https://i.stack.imgur.com/YEjyE.png -> correctly displays overload #3 // https://i.stack.imgur.com/04SKl.png -> wrongly displays overload #1 – hymced Dec 07 '17 at 14:12
0

ANSWER to Subsidiary question 1:

Because arrays that are part of structs or classes do not decay (when the whole struct or class is passed to a function for example).

source: http://www.learncpp.com/cpp-tutorial/6-8-pointers-and-arrays/

ANSWER to MAIN QUESTION 1:

As Smeehey pointed it out, the correct way seems to be using singledata(std::move(val1)) in the member initializer list. And I think Visual Studio (2017) is simply just not displaying the correct overload, as demonstrated here: std::move of string literal - which compiler is correct?

hymced
  • 570
  • 5
  • 19