2

Since I'm a beginner in c++, some questions don't quite understand. This question came across by accident while I was reading C++ primer 5th.


I have a Cat class with 3 different constructors(named by Constructor 1, Constructor 2, Constructor 3):

class Cat {
    friend class Cats;

private:
    std::string name;

public:
    Cat() = default;

    // Constructor 1
    Cat(std::string n): name(n) {
        std::printf("Call: Cat(std::string n)\n");
    }

    // Constructor 2
    Cat(std::string& n): name(n) {
        std::printf("Call: Cat(std::string& n)\n");
    }

    // Constructor 3
    Cat(const std::string& n): name(n) {
        std::printf("Call: Cat(const std::string &n)\n");
    }

};

And I want to instantiate the class in two different ways:

class C7T17_Main {
public:
    void run() {

        // Instantiation 1
        Cat cat(std::string("Cathy"));
       
        // Instantiation 2
        std::string s("Mike");
        Cat cat2(s);

    }
};

Then the problem comes:

  • For Constructor 1:

    • Instantiation 1 and Instantiation 2 both work well
  • For Constructor 2:

    • Instantiation 1 raises an error. The compiler complains that 'Candidate constructor not viable: expects an lvalue for 1st argument'
    • Instantiation 2 works normally.
  • For Constructor 3:

    • Instantiation 1 and Instantiation 2 both work well

My guess is:

  • Instantiation 1 does not actually create a variable, but a temporary value for initializing cat, so it is not suitable as a reference parameter.

  • For constructor 3, the const reference parameter represents a constant that can accept a temporary value for initialization


Looking forward to your help. :)

黄柏禧
  • 23
  • 3
  • 1
    ***'Candidate constructor not viable: expects an lvalue for 1st argument'*** You get this error, because you passed anonymous string to a function expecting a reference argument. Reference requires the string to be stored in some variable. So your guess is correct. – kiner_shah Jan 10 '22 at 11:32
  • You are right. Though MSVC with `/Zc:referenceBinding-` would compile `Constructor 2` too – Alex Guteniev Jan 10 '22 at 11:32
  • ***For constructor 3, the const reference parameter...*** not sure about this, but maybe because the argument expected is a "reference to a const string". Since the value isn't going to be modified, maybe compiler allows it - but not sure. – kiner_shah Jan 10 '22 at 11:36
  • 2
    @kiner_shah You can always bind a `const` reference to a rvalue (assuming of course the types match). That is just a rule of the language. – user17732522 Jan 10 '22 at 12:36
  • @user17732522, got it thanks :-) – kiner_shah Jan 11 '22 at 04:27

1 Answers1

3

It's pretty simple:

  1. ctor 1 receives the argument by copy (pass by value);
  2. ctor 2 receives the argument by non-const lvalue reference, so it only supports non-const lvalues.
  3. ctor 3 receives argument by const lvalue reference so it supports const-lvalue, non-const lvalue, const rvalue and non-const rvalue.

Instantiation 1 does not actually create a variable, but a temporary value for initializing cat, so it is not suitable as a reference parameter.

Yes, it creates a prvalue, and it can be passed by reference like this:

// ctor 4
Cat( std::string&& n ) // see the double ampersand

ctor 4 only accepts temporaries (rvalues) by reference.

For constructor 3, the const reference parameter represents a constant that can accept a temporary value for initialization

Yes, it can bind to all the values as I mentioned previously.

For Constructor 2: Instantiation 1 raises an error. The compiler complains that 'Candidate constructor not viable: expects an lvalue for 1st argument'

This is the expected behavior.

Important Note:

Cat(std::string&& n): name(n) { // here n is of type rvalue refernece to string
    std::printf("Call: Cat(std::string& n)\n");
}

Function parameters that are of type rvalue reference are lvalues themselves. So in the above code, n is a variable and it's an lvalue. Thus it has an identity and can not be moved from (you need to use std::move in order to make it movable).

Extra note:

You can even pass an lvalue to a function that only accepts rvalues like this:

Cat( std::string&& n );

std::string s("Mike");
Cat cat2( std::move(s) );

std::move performs a simple cast. It casts an lvalue to an xvalue so it becomes usable by a function that only accepts an rvalue.

Value Category in C++11

Take a look at this: Value Categories in C++11

Explanation for the above image

In C++11, expressions that:

  • have identity and cannot be moved from are called lvalue expressions;
  • have identity and can be moved from are called xvalue expressions;
  • do not have identity and can be moved from are called prvalue ("pure rvalue") expressions;
  • do not have identity and cannot be moved from are not used[6].

The expressions that have identity are called "glvalue expressions" (glvalue stands for "generalized lvalue"). Both lvalues and xvalues are glvalue expressions.

The expressions that can be moved from are called "rvalue expressions". Both prvalues and xvalues are rvalue expressions.

Read more about this topic at value categories.

digito_evo
  • 3,216
  • 2
  • 14
  • 42