c++ const 成员函数 & 临时变量 & 右值引用 & move

const 成员函数

尊重原作,部分转自http://blog.csdn.net/lihao21/article/details/8634876

我们知道,在C++中,若一个变量声明为const类型,则试图修改该变量的值的操作都被视编译错误。例如:

const char blank = 'a';  
blank = 'b';  // 错误  

面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证const对象的常量性,编译器须区分不安全与安全的成员函数(即区分试图修改类对象与不修改类对象的函数),例如:

const Screen blankScreen;  
blankScreen.display();   // 对象的读操作  
blankScreen.set('*');    // 错误:const类对象不允许修改, but but but...这个不允许被修改并不代表 const 成员函数就一定线程安全, see Effective Modern c++ Item 16 

在C++中,只有被声明为const的成员函数才能被一个const类对象调用。

要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const,例如:

class Screen {  
public:  
   char get() const;  
};

在类体之外定义const成员函数时,还必须加上const关键字,例如:

char Screen::get() const {  
   return _screen[_cursor];  
}  

若将成员成员函数声明为const,则该函数不允许修改类的数据成员。例如:

class Screen {  
public:  
int ok() const {return _cursor; }  
int error(intival) const { _cursor = ival; }  
};  

在上面成员函数的定义中,ok()的定义是合法的,error()的定义则非法。
值得注意的是,把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针(注:bitwise constness和logical constness),则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。例如:

class Name {  
public:  
void setName(const string &s) const;  
private:  
    char *m_sName;  
};  
  
void setName(const string &s) const {  
    m_sName = s.c_str();      // 错误!不能修改m_sName;  
  
for (int i = 0; i < s.size(); ++i)   
    m_sName[i] = s[i];       // 不好的风格,但不是错误的  
}  

虽然m_Name不能被修改,但m_sNamechar *类型,const成员函数可以修改其所指向的字符。

const成员函数可以被具有相同参数列表的const成员函数重载,例如:

class Screen {  
public:  
char get(int x,int y);  
char get(int x,int y) const;  
};  

在这种情况下,类对象的常量性决定调用哪个函数。

const Screen cs;  
Screen cc2;  
char ch = cs.get(0, 0);  // 调用const成员函数  
ch = cs2.get(0, 0);     // 调用非const成员函数  

小结:

  • 1)const成员函数可以访问非const对象所有数据,也可以访问const对象内的所有数据成员;
  • 2)非const成员函数可以可以访问非const对象所有数据,但是不可以访问const对象内的任何数据成员;
  • 3)作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。
  • 4)此外,尽量将不会被修改的函数参数声明为 const, 因为这样的函数也可以将 临时对象 作为参数,否则会编译不通过。

函数临时变量:

Question:
When creating a new instance of a MyClass as an argument to a function like so:

class MyClass
{
  MyClass(int a);
};    
myFunction(MyClass(42));

does the standard make any grantees on the timing of the destructor?
Specifically, can I assume that the it is going to be called before the next statement after the call to myFunction() ?

Answer:
Temporary objects are destroyed at the end of the full expression they are part of.

A full expression is an expression that isn’t a sub-expression of some other expression. Usually this means it ends at the ; (or ) for if, while, switch etc.) denoting the end of the statement. In your example, it’s the end of the function call.

Note that you can extend the lifetime of temporaries by binding them to a const reference. Doing so extends their lifetime to the reference’s lifetime:

MyClass getMyClass();

{
  const MyClass& r = getMyClass(); // full expression ends here
  ...
} // object returned by getMyClass() is destroyed here

If you don’t plan to change the returned object, then this is a nice trick to safe a copy constructor call (compared to MyClass obj = getMyClass();) which unfortunately isn’t very well known. (I suppose C++11’s move semantics will render it less useful, though.)

see link: http://stackoverflow.com/questions/2506793/c-life-span-of-temporary-arguments

右值引用:

当你函数返回的值是一个 ,而不是一个 引用 的时候,就会出现一个临时对象了,所以大家都很喜欢传递引用(而不是值),以下讨论都是基于传递引用的情况。
但是在c++11以前你只能通过一个 const 引用去绑定一个临时对象(或者叫右值——对应的可以取地址的,并且有名称的变量叫坐值,例如 int a),否则就编译不通过,const 就意味着你不能对该临时对象进行任何修改了。

假设一种情况,可以修改临时对象,例如:在以一个临时对象为参数构造一个新的对象时,假设那个临时对象new了一块内存,我们在新的对象中可以直接复制该临时对象new了内存指针,然后将那个临时对象的指针 修改 为 NULL, 这样该临时对象析构的时候就会delete一个NULL指针——内存还保留着,而实际分配的内存就被转移到了我们这个新的对象中,这样是不是就很有效率了呢?

这里有两个问题: 第一个就是如何判断该对象是临时对象,第二个就是该临时对象不能是const类型的。

c++11中的右值引用(rvalue reference)解决了这两个问题!

// 如果func参数是引用(避免复制),则必须为 const 引用
func(std::string("abc"));  // 传入的是个临时对象(rvalue), 该临时对象的生命周期 see above

std::string str("abc");
func(str)  // 传入的不是临时变量(lvalue)

Prior to C++11, if you had a temporary object, you could use a “regular” or “lvalue reference” to bind it, but only if it was const:

const string& name = getName(); // ok
string& name = getName(); // NOT ok

The intuition here is that you cannot use a “mutable” reference because, if you did, you’d be able to modify some object that is about to disappear, and that would be dangerous. Notice, by the way, that holding on to a const reference to a temporary object ensures that the temporary object isn’t immediately destructed. This is a nice guarantee of C++, but it is still a temporary object, so you don’t want to modify it.

In C++11, however, there’s a new kind of reference, an “rvalue reference”, that will let you bind a mutable reference to an rvalue, but not an lvalue. In other words, rvalue references are perfect for detecting if a value is temporary object or not. Rvalue references use the && syntax instead of just &, and can be const and non-const, just like lvalue references, although you’ll rarely see a const rvalue reference (as we’ll see, mutable references are kind of the point):

const string&& name = getName(); // ok
string&& name = getName(); // also ok - praise be!

你编写的 move 函数能够被成功调用还要有两个条件: 1, 必须传入一个右值 2, 该右值能够被修改(即不是const)
std::move就是保证这两个条件能够满足,(std::move 的作用就是将一个变量强制类型转换右值引用 类型)
例如:

std::vector<int> a = {1,2,3,4,5};
std::vector<int> b = {3,2,1};
b = a;  // 调用operator= 复制各个元素
b = std::move(a); // 调用 move operator 移动各个元素,高效!所有的容器元素都是通过 new 出来的,所以move用在这里再好不过了。不过此时 a 就变成了空vector了.

std::string mystring("hello world");  // string 底层也是通过 new 出来的
std::vector<std::string> myvector;

myvector.emplace_back(mystring);  // 这是复制构造,相当于 .push_back(mystring)
myvector.push_back(std::move(mystring));  // 这是 move,高效

但是注意,如果被move的对象是一个flat type数据结构(如 int,double 等),不包含指针成员指向new出来的内存空间,则 std::move 并没有任何效率提高。

例程 see link:
https://stackoverflow.com/questions/11572669/move-with-vectorpush-back
https://stackoverflow.com/questions/26860749/efficiency-of-c11-push-back-with-stdmove-versus-emplace-back-for-already

如下图(ref. effective modern c++),move 一个 vector 相当于移动一个指针(vw1 变为 null):
这里写图片描述
还有一个注意点: 在多层右值函数调用的时候,每一层函数参数都需要 显示 强制类型转换(用std::move函数)为 右值引用 (因为在函数内部,该参数只是一个引用——是一个左值,不是右值),否则下层函数调用将会重载参数为 const 引用左值 的版本,而不是右值版本。

Put a final way: both lvalue and rvalue references are lvalue expressions. The difference is that an lvalue reference must be const to hold a reference to an rvalue, whereas an rvalue reference can always hold a reference to an rvalue. It’s like the difference between a pointer, and what is pointed to. The thing pointed-to came from an rvalue, but when we use rvalue reference itself, it results in an lvalue.

不过, rvalue references cannot magically keep an object alive for you, 所以这样是不可行的( 与返回 lvalue 引用是类似的,不能返回临时对象的引用 ):

int && getRvalueInt ()
{
	int x = 123;
    return std::move( x );
}

总结下来, 左值引用 和 右值引用 一样,都只是个引用(是个右值),不同点是,左值引用只能绑定到左值右值引用可以绑定到右值,并且左值常量引用也可以绑定到右值

所以说,右值引用 和 std::move 对那种包含一大块内存的 临时对象 具有很大的利用率。尤其是对于需要在内部new一些内存的对象(如vector, 很长的 string等等)。对于这些对象,自己要写 move 构造函数 和 move operator(对于自己编写的类,如果实现了copy operations, move operations, or destructors 这里头的任何一个函数,编译器就不会默认自动生成这些函数了,需要自己挨个实现——or 显示写=default)。 而且要考虑自己写的哪些函数会影响编译自动生成的函数(默认构造,默认复制,默认赋值 等等等)。
c++11所有的 stl 容器都已经实现了 move constructor 和 move assignment operator, 在使用标准容器的时候可以可以考虑优化一下代码。

函数返回值不需要被move

另外需要注意的是,在函数返回值时,不要使用 std::move,因为编译器会做一些 NRVO 优化工作:

The compiler tries to elide copies, invokes a move constructor if it can’t remove copies, calls a copy constructor if it can’t move, and fails to compile if it can’t copy.

所以,要么已经去掉了复制(eliminate the copy and initialize the return value directly from the return expression),要么隐式的帮你把临时变量转成了右值(会调用 move 构造,需要定义move构造函数), see link(非常好的链接,一定不能错过):

  1. https://stackoverflow.com/questions/17473753/c11-return-value-optimization-or-move
  2. https://stackoverflow.com/questions/4316727/returning-unique-ptr-from-functions
  3. https://isocpp.org/blog/2014/09/return-unique-ptr-by-value
  4. https://blog.knatten.org/2011/08/26/dont-be-afraid-of-returning-by-value-know-the-return-value-optimization/
    所以,下面这样的代码是没必要的:
// explicit move
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );  // 假设 SerialBuffer 支持move
    read( begin( buffer ), end( buffer ) );
    return std::move( buffer );  // 没必要,编译器会进行合适优化,包括使用move语意。直接写成 return buffer 即可。
}

其他参考链接:

  1. http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
  2. http://eli.thegreenplace.net/2011/12/15/understanding-lvalues-and-rvalues-in-c-and-c
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值