c++23中的新功能之六更简单的隐性Move

一、介绍

在学习c++的过程中,一些老的技法,其实在不断的被自动优化。比如一些循环的优化、字符串的处理等等。随着标准的发展,编译器的跟进,有些优化其实编译器可以做的更好,比如一些字符串处理过程中的临时变量。但是c++毕竟是一门中高级语言,不可能做到面面俱到。这里面仍然有大量的优化需要开发者自己承担,不过,随着更新的c++标准不断提出,又有一些优化的可能被编译器实现了。那就是今天本文提到的“Simpler Implicit Move”。
这里先引入一些基础,需要大家回忆一下在什么情况下会出现复制对象的可能,哪种更频繁。换句话说,哪里有可能出现内存的重复应用,那就有优化的空间,不管他是不是必须的。因为这种必须可能是现在必须,未来未必。

二、返回值的优化

在早期的c++的函数返回值的处理中,可能有印象的早期的学习c++的朋友都知道,返回值是要复制三次的,这对于一个普通的基础类型如int,char之类的还可以忽视,但对于一个拥有数据较多的类对象本身就是一种内存的浪费,所以才有了NRVO,返回值优化。其实这是Copy Elision,复制省略的一种方式。这和前面提到过的COW和零拷贝目的都是一样的,内存毕竟宝贵,省一下是一下,省一回是一回。看下面的一个例子:

struct A
{
    A() {};
    A(const A&) {};
    A(A&&) noexcept {};
};
A Create()
{
    return A{};
};
A Inst()
{
    A a = Create();
    return a;
}
void Get()
{
    A a1 = Inst();
}

这里面有拷贝构造函数和移动构造函数,对于这种non-trivial的行为,c++的中会产生多次拷贝和移动。但是这时,编译器就出手了。先分析一下,如果从代码上分析,A构造会产生一次,然后再移动构造一次,Create后返回被拷贝一次,最后a1再来一次移动构造。然后在c++11标准后的编译器,发现有的处理其实是在Get()函数的栈帧上,所以它直接掠过当中的拷贝把a返回到a1中,省略一次拷贝。其实编译器不但会省略这一步,还会进一步把Create函数的结果直接拷贝到a1,这在c++11以前的编译器中,就称为前面说的NRVO。但是它有着一些比较严格的媱,比如返回值不能有修饰,比如move之类的,这就没办法NRVO了。另外,要求返回值必须有明确的拷贝构造函数且必须是同一类(父子类都不行)
而在c++11中,则提出implicit move,其实就是对移动处理这些,即先把返回值定做右值进行重载,成功就NRVO,不成功则回到原来的NRVO规则。


std::unique_ptr Get()
{
    std::unique_ptr uPtr =  Inst();
    return uPtr;
}

三、c++14以后的变化

技术标准只要开了口子,在不会有威胁时,只会越开越大,c++14以后进一步进行了放松,即对返回同类型也不要求必须完全一致了,即下面也是可以的:

struct P { };
struct S : public P { };
std::unique_ptr

Get() { std::unique_ptr uPtr = Inst(); return uPtr; }

原来的标准中,复制省略在不同的类型中是完全不起作用的,但是在c++14后,将其看做右值,利用移动构造函数来返回。
在c++20后,继续放松,但它更倾向于复制省略而非implicit move,看一个例子:

struct S {};
struct S1 { S1(S ss){}; };//S1接受S的值来构造
S1 Get() { S s; return s; }

在c++17中,在Get函数中,s会被先拷贝,然后调用S1的构造函数,因为S1没有&&移动构造函数(这是c++11中implicit move严格接受右值,必须有S1(S&&)这种签名才可),但是在c++20 中则进一步放松,此处即可调用S的移动构造函数,然后再调用S1的构造函数。
但是有没有发现,下面的例子却编译不过:

struct S {};
struct S1 { S1(S &&ss){}; };
S&& Get() { S s; return s; }

不行的原因是,c++中怎么可以允许你返回一个悬垂引用(野指针)。

等到了c++23,则又出现了新的问题,这个问题还是上面的这个问题引出的,看下面的例子:

struct S {};
struct S1 { S1(S&&){}; };
S1 Get1(S&& s) { return s; }
//下面的错误
S&& Get2(S&& s) { return s; }

刚刚在前面说过的话,只要能省略的,都有优化的可能。这里的代码不再有NRVO,只是直接把s返回,如果可以,这就有点香了。但一个左值变成右值需要std::move,这从一个右值变成左右再变成右值,明显是在玩儿闹啊。标准制定者后终决定,只要可以move的return值,直接把返回值做为右值。但是这个有点小瑕疵,看下面的代码:

int& Example() {int a = 0; return a;}   (1)
int&& Example() {int a = 0; return a;}  (2)

如果认真学习过c++的基础语法的都明白,(1)式是明显的返回一个临时变量的引用(悬垂引用,这个和野指针是一个道理),这玩意儿闹大了啊,但在c++23中这个玩意儿合法了。惊不惊喜,意不意外。但(2)式仍然是个坏孩子,返回一个悬垂引用。

四、总结

通过对上面的隐性移动的不断扩展的路程可以看到,标准的演化,仍然是前面分析过的,不断的朝着简单,填坑的方向发展。在尽量易用的前提下,把特殊情况不断的排除。这也是c++原来固有的一个毛病,大多数可以,有几种情况不可以,有些开发者可能一辈子都遇不到这些情况,但面试时还得被虐。话又说回来,标准发展着就发现,有些东西还是得补洞,不然会有新的窟窿出来。完美的事情,毕竟是不存在的。

你可能感兴趣的:(C++11,C++,c++23,c++,算法)