About
RSS

Bit Focus


C++ 2011 中的 Left Value 与 Rvalue Reference

Posted at 2011-03-28 13:07:31 | Updated at 2018-09-21 07:03:47

#include "某 C++ 概念之序篇"

答: std::stringstream() 产生的临时变量无法匹配类型 std::stringstream&.

    但是 std::stringstream() 可以匹配类型 std::stringstream const&, 也就是说, 如果函数 std::stringstream& operator<<(std::stringstream& os, double d) 的签名和实现作修改成下面这个样子就行了
std::stringstream const& operator<<(std::stringstream const& os, double d)
{
    const_cast<std::stringstream&>(os).operator<<(d);
    return os;
}

    之前扯过 C++ 已经没有语法意义上的左值了, 不过这里似乎有一点点很微妙的, 跟左值很类似的东西, 那就是, C++ 引用. 任何一本 C++ 入门书籍里面都会花一些篇幅来讨论引用, 其基本意义, 无非是某个东西的别名, 因此, 这某个东西对于引用而言显然是至关重要的.
    最基本的一点是, 如果被引用的东西是临时变量 (顺便一提, 如果是字面常量这种比较尴尬的情况, 视同临时变量) 怎么办? 比如之前代码样例 A 中的情况?
    这就牵扯到 C++ 中正统的 left value. 因为 left 已经不再有任何 "左" 相关的意义的, 所以我不太想在本文中把这货称之为 "左值" (实际上英文在这些方面更加灵活, 比如 Copyleft 这词跟 left 也没太大关系, 用来吐槽则非常不错).

    一个东西如果是 left value, 那么它就可以被赋值给 left value 引用, 所谓 left value 引用嘛, 就是就是类似 std::stringstream& 的引用类型, 而暗含 const 的类型的引用则不算
typedef int const int_const;
int_const& this_is_not_a_left_value_reference;
    那究竟什么是 left value 呢?
    一个语句之后仍然存在的变量, 表达式, 或者这样类似的 "具有值属性的什么东西" 就是 left value. 而
std::cout << (std::stringstream() << d).str().length() << std::endl;
这句之后, 显然临时构造的 std::stringstream() 就被析构了, 因此这家伙不算是 left value, 自然也就无法被 std::stringstream& 所引用.
    那常数为什么不算 left value 呢? 当然, 可以认为常数居阴阳之外, 别谈语句之后, 吐核退出, 就算是宇宙毁灭, 什么 π 啊 e 啊还活得好好的; 不过, 从计算机的角度来理解, 常数是 CPU 指令的立即数, 所以别谈语句之后, 只要这条指令结束, 它就没了.

    然而, C++ 一个很邪恶的规定是, 即使一个东西不是 left value, 它仍然可以绑定 const 引用, 比如以下语句都是合法的
std::pair<int, int> const& pair = std::make_pair(0, 1);
std::stringstream const& strm = std::stringstream();
std::string const& x = "Raki suta";
int const& i = 0;
    这样直白地写出来是有点让人觉得不舒服, 不过作为函数调用的参数则好得多, 比如之前的代码片段 B 那样
std::string const& lookup(std::map<std::string, std::string> const& map
                        , std::string const& key)
            throw(key_error)
{
    std::map<std::string, std::string>::const_iterator i = map.find(key);
    if (map.end() == i) {
        throw key_error("key “" + key + "'' not found in map");
    }
    return i->second;
}
    这时下面的调用就显得非常自然了
std::map<std::string, std::string> map /* ...... */;
lookup(map, "Kuroi"); // temporary value to const reference, OK

std::string key("Shiraishi");
lookup(map, key); // left value to const reference, OK
    不过, 代码片段 B 中的问题也正是因为这个, 函数 lookup 的设计可能导致崩溃, 不是那种把异常捕捉给忘记而导致的崩溃, 而是比那更加险象环生, 难以调试的内存错误, 比如下面这种使用
std::map<std::string, std::string> prepare_map()
{
    std::map<std::string, std::string> map;
    map["Konata"] = "Izumi";
    map["Kagami"] = "Hiiragi";
    map["Tsukasa"] = "Hiiragi";
    map["Miwiki"] = "Takara";
    return map;
}

int main()
{
    std::cout << lookup(prepare_map(), "Konata") << std::endl; // Ok
    std::string const& value = lookup(prepare_map(), "Konata");
    std::cout << value << std::endl; // CRASH
    return 0;
}
    问题的本质在于 const 修饰的引用即可以绑定 left value 引用, 又可以绑定非 left value 引用, 结果实际上变同时具有了 left value 引用和非 left value 引用性质, 危险之处在于, 这种桥接看起来非常自然又暗藏危险: 它可以绑定一个非 left value 引用, 然后做 left value 引用的事情.
    那么是否有什么东西是彻底排斥 left value 的呢?
    有! 请看新标准.

    在 C++0x (最新消息, 标准被委员会决议通过, 正式命名为 C++ 2011 Standard, 对此槽已经吐过了) 引入复杂的 move semantic 就是 (尝试) 解决这个问题的手法. 之前在 C++ 转移构造std::unique_ptr<cookbook> 中提到的都只是 move semantic 的具体应用, move semantic 的根基是一个称为 rvalue reference 的东西. 虽然 rvalue 这个会让人联想到 right value 这个词, 不过 right 就像上面说的 left 一样很暧昧. 下文中将简称 rvalue reference 为 rref.
    回到正题, 在 C++ 新标准中, 如果一个值表达式不是 left value, 那么它就一定是 rref, 如果它是 left value, 那么它就一定不是 rref. 也就是说这两者是二中取一 (当然, const 引用还是那一副死相, 仍为两者的并集).
    话不多说, 下面上代码, 在代码片段 B 增加下面函数
std::string lookup(std::map<std::string, std::string>&& map
                 , std::string const& key)
            throw(key_error)
{
    std::map<std::string, std::string>::const_iterator i = map.find(key);
    if (map.end() == i) {
        throw key_error("key “" + key + "'' not found in map");
    }
    return std::move(i->second);
}
    再把刚才导致崩溃的 main 函数改成
int main()
{
    std::cout << lookup(prepare_map(), "Konata") << std::endl;
    std::string value(std::move(lookup(prepare_map(), "Konata")));
    std::cout << value << std::endl;
    return 0;
}
    如果不放心的话还可以把原来的 lookup(std::map<std::string, std::string> const& map, std::string const& key) 删掉, 反正也没有用到.

    可不可以不这么麻烦, 干脆把原来的 lookup(std::map<std::string, std::string> const& map, std::string const& key) 什么的全部改成复制返回不就行了?
    当然例子中使用的是 std::string 这种可以轻松复制的东西, 假如是某些不可复制的对象, 上面的策略就不行了.
    其实还有一个原因, 效率, 准确地说, 是减少一次复制构造的开销.
    在上面改进后的例子中, 实际上没有任何复制发生, 从 prepare_map 传递到 lookup 的是 rref, 从 lookup 返回是通过转移的; 而之前的 lookup 即使改成返回对象而非引用, 也不可避免发生一次对象复制, 这里 NRV 优化是不可能实施的 (也不用担心编译器是否有足够的能力去优化 NRV --- 既然它连 C++ 2011 这么变态的标准都实现了, 还会在乎一个破 NRV 优化?).

    最后一个问题, 有没有什么方法能完全不允许 rref 传递? 即禁止类似 lookup(prepare_map(), "Konata") 的调用呢?
    有! 还记得转移构造中提到的 deleted function 么? 它开可以用来删除全局函数哦, 比如给出下面的定义
std::string lookup(std::map<std::string, std::string>&&, std::string) = delete;
    现在再编译, 编译器就会提示函数已经删掉咯.

写这篇文章参考的工具和环境为 g++ 4.5.2 (ArchLinux GCC 4.5.2), 文中出现的所有符合 C++ 2011 Standard 的代码均可使用 g++ -std=c++0x 编译通过.

Post tags:   Move Semantic  Operator Overload  C++  Right Reference  Copy Constructor  Left Value  C++11

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site