97
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

みんなlvalueとrvalueを難しく考えすぎちゃいないかい?

Last updated at Posted at 2017-06-26

読者がまずやるべきこと

  1. 「右辺値」「左辺値」という言葉を忘れる。それらは言葉そのものが間違いだ
  2. もし読者であるあなたがC++の規格書を読んだことがないならば、value categories, prvalue, xvalueという言葉を忘れる。コードを書く上でひたすら有害である。

lvalueとrvalue

そう、規格書ではlvalueとrvalueとなっている。「右辺値」「左辺値」というのは誤訳だ(正確には時代遅れ)、もう一度言うが直ちに脳内から消去するべきである。

lvalueとrvalueとはいずれもオブジェクトだ。オブジェクトという言葉が聞き慣れないなら、初心者向けのC言語本でみかける、箱、とか容器とか入れ物でもいい。

では何が違うのか。誤解を恐れずにまとめてみよう。

lvalue := 名前のあるオブジェクト

rvalue := 名前のないオブジェクト

もう一度言おう。lvalueとrvalueの違いは名前があるかないか、ただそれだけだ。

rvalueの例

名前がないならrvalueだ。

struct Hoge {};
int f() { return 3; }
int&& g() { return 4; }
int main() {
  //以下rvalue
  3;
  2.4;
  f();
  g();
  Hoge{};
}

lvalueの例

名前、もっというと変数名がついていればlvalueだ。

int&& g() { return 4; }
int a;//lvalue
static double b;//lvalue
namespace {
  char c;//lvalue
}
int main() {
  unsigned int n;//lvalue
  int&& n = g();//lvalue
}

lvalueとrvalueの寿命(life time)

種類 寿命
static storage(グローバル変数や関数内static変数) プログラムの終了まで
自動変数(いわゆるstack変数) スコープの終わりまで
rvalue 式の終わりまで
int f() { return 5; }
int a = 0;//プログラムの終了まで
static double b = 3.2;//プログラムの終了まで
int main()
{
  static char c = 2;//プログラムの終了まで
  {
    int n = 0;//スコープの終わりまで
    f();//式の終わりまで
  }
}

rvalueの寿命が延長されるとき

ものすごくわかりにくいし混乱しやすいからいっそ忘れてくれてもいい、と言いたい。

int f() { return 3; }
int main()
{
  int&& n1 = f();
  const int& n2 = f();
}

f()の評価結果は明らかにrvalueだ。これを参照型変数にひも付けている。このときもしf()が返すrvalueが式の終わり、つまりひも付けた直後に寿命が尽きてしまうと仮定すると、参照型変数は亜空間の別名ということになり、NULL referenceとかいうありえない状態になってしまう。
なのでそんなことはなくちゃんとひも付けた参照型変数の寿命に等しい、という規定がある。

でここまでならわかりやすいのだが、これはすべてのrvalueについて言えるわけではない

で、結論としては、関数の引数以外でrvalue referenceを使うなということです。

右辺値参照型変数の参照先を解放してはいけない

rvalueならば所有権を持たないという社会的合意

rvalueはchainすることはあるかもしれないが、最終的に変数に代入するか変数を初期化するかのいずれかに用いることが基本的には多い。

なので所有権という新たな概念を導入し、rvalueならば所有権を持たないという社会的合意を形成する。

所有権を持たないならば対象オブジェクトをいかに書き換えようとも文句は言われない。つまりここで、rvalue referenceで受け取ったものは自由にいじくり回していい、という社会的合意を行う。

#include <iostream>

class Hoge;
void disabler(Hoge&& h);
class Hoge {
private:
  bool is_disabled;
public:
  Hoge() = default;
  void foo()
  {
    if(!this->is_disabled) {
      //大事な処理
      std::cout << "大事な処理が実行されました" << std::endl;
    }
  }
  friend void disabler(Hoge&& h);
};
void disabler(Hoge&& h)
{
  //hそのものはlvalueだがhの実態はrvalue
  //Hoge::foo内の大事な処理が実行されないようにフラグを変更しちゃう
  h.is_disabled = true;
  std::cout << "大事な処理を殺してやった" << std::endl;
}
void disabler(const Hoge&)
{
  std::cout << "大事な処理は生きています" << std::endl;
}
int main()
{
  Hoge h = {};
  //大事な処理は実行される
  h.foo();// => 大事な処理が実行されました

  //hはlvalueなのでvoid disabler(Hoge&&)の方ではなく
  //void disabler(const Hoge&)の方を呼び出す
  disabler(h);// => 大事な処理は生きています

  //大事な処理は実行される
  h.foo();// => 大事な処理が実行されました

  //rvalue referenceへのキャストの評価結果はrvalueなので
  //void disabler(const Hoge&)の方よりも優先度が高い
  //void disabler(Hoge&&)の方を呼び出す
  disabler(static_cast<Hoge&&>(h));// => 大事な処理を殺してやった

  //大事な処理は実行されない
  h.foo();
}

rvalueならばmoveしてよいという社会的合意

move semanticsとは

動的確保した領域を指すなどしている、ポインタがクラスメンバーにあるとき、このポインタを単純にコピーすること(新たにメモリー確保してポインタの指す先までcopyしたりはしない)。

お前のものは俺もの、というジャイアン方式だ。

rvalueならばmoveしてよい

moveはいつでも安全なわけではない。ポインタがいつの間にか無効になっていたなんてことが起こるえるので発見困難なバグになる。ではいつならmoveしていいのか?所有権を持っていないオブジェクトに対してだ。言い換えると、rvalue referenceに対してはmoveを行っても安全である

なぜか?rvalueならば所有権を持たないという社会的合意があるからだ。

#include <cstring>
#include <cstddef>
#include <iostream>
class inferior_string
{
public:
    inferior_string() noexcept : m_s_(nullptr), m_len_(0), m_capacity_(0) {}
    inferior_string(const char* str)
    {
        const std::size_t len = (nullptr == str) ? 0 : std::strlen(str);
        if(0 == len){
            this->m_s_ = nullptr;
            this->m_len_ = this->m_capacity_ = 0;
        }
        else{
            const std::size_t cap = 2 * len;
            this->m_s_ = new char[cap]();
            std::memcpy(this->m_s_, str, len);//copy

            this->m_len_ = len;
            this->m_capacity_ = cap;
        }
    }
    inferior_string(const inferior_string& o)//copy constructor
    {
        if(0 == o.m_len_){
            this->m_s_ = nullptr;
            this->m_len_ = this->m_capacity_ = 0;
        }
        else{
            const std::size_t cap = o.m_len_ * 2;
            this->m_s_ = new char[cap]();
            std::memcpy(this->m_s_, o.m_s_, o.m_len_);//copy
            this->m_len_ = o.m_len_;
            this->m_capacity_ = cap;
        }
    }
    inferior_string(inferior_string&& o) noexcept
    : m_s_(o.m_s_), m_len_(o.m_len_), m_capacity_(o.m_capacity_)//move constructor
    {
        o.m_s_ = nullptr;//disable input object's destructor. DO NO FORGEET!!!
    }
    ~inferior_string()
    {
        delete[] this->m_s_;
    }
    const char* c_str() const noexcept { return this->m_s_; }
private:
    char* m_s_;
    std::size_t m_len_;
    std::size_t m_capacity_;
};
std::ostream& operator<< (std::ostream& os, const inferior_string& str){
    os << str.c_str();
    return os;
}
int main()
{
    inferior_string str = "arikitari";
    inferior_string str2 = str;//copy constructor call
    std::cout << str << ", " << str2 << ", ";
    inferior_string str3 = std::move(str);//move constructor call
    //inferior_string str3 = static_cast<inferior_string&&>(str);//同じ意味
    std::cout << str3 << std::endl;
    return 0;

}

lvalueの所有権を放棄したと偽装するにはrvalue referenceにキャストすれば良い。さっきから言っているように、rvalue referenceへのキャスト式の評価結果はrvalueだ。

しかしstatic_cast<inferior_string&&>(str)のようなキャストを毎回書くのはめんd。プログラマーは怠惰であるべきだ。

ここで先に述べた、rvalue referenceを返す関数の呼び出しの評価結果はrvalueである、という性質を利用する。つまりつぎのようなラッパー関数が書ける。

inferior_string&& move(inferior_string& str)
{
  return static_cast<inferior_string&&>(str)
}

しかしmoveしたいすべてのクラスに対してこういうラッパー関数を書いて回るのは馬鹿げている。そこでtemplateの出番だ。

utility_C++14
#include <type_traits>

namespace std {

template <class T>
constexpr typename std::remove_reference<T>::type&& move(T&& t)
{
  return static_cast<std::remove_reference<T>::type&&>(t) ;
}

}

こういうラッパー関数がSTLには存在する。利用するには#include <utility>する必要がある(が大体他のヘッダを読み込むともれなく読み込まれている)

ただし、さっきから言っているようにstd::moveはただのキャストなのでもちろんmove semantics以外の目的にも使える。しかし可読性の観点からそうするべきではない。

std::moveは使用上の注意をよく読み、用法・用量を守って正しくお使い下さい。

あとそもそもの話でrvalue referneceを受け取る引数がないものにstd::moveつかって渡すのは意味がない。むしろあかん。

std::moveはムーブとは限らない

「lvalueとrvalueの違いは名前があるかないか」の例外

まあ歴史的経緯とか現実的な処理系との兼ね合いとかそもそも説明を簡略化しすぎた弊害というか、流石にこの説明には穴がある。

#include <iostream>

int& f1(int& n){ return n; }
void f2(int) {}
template<typename Base>
struct Interface {
  void print()
  {
    //lvalue referenceへのキャスト式の評価結果はlvalue
    static_cast<Base&>(*this).print();
  }
};
class myclass1 : Interface<myclass1> {
public:
    void print(){ std::cout << "myclass1" << std::endl; }
};
int main()
{
  int n = 3;
  int arr[3] = {};
  //arrがlvalueなのでそれのoperator[]の評価結果も
  //lvalue referenceを返す系組み込みoperatorなのでlvalue
  arr[1];
  //lvalue referenceを返す関数の呼び出しの評価結果はlvalue
  f1(n);
  //関数型へのrvalue reference型へのキャスト式の評価結果はlvalue
  static_cast<void (&&)(int)>(f2);
  //文字列リテラルはlvalue
  "arikitari";
  //operator*の評価結果はlvalue
  *(new int());
}

operator*以外にも例には書いていないが、参照を返す組み込みoperator全般も概ね例外となる。

関数型へのrvalue reference型へのキャスト式とかいういつ使うのか謎なものは忘れるとして、lvalue referenceは名前の有無にかかわらずlvalueであるということだろう。

文字列リテラルはちょっと考えればわかるが、あれは殆どの処理系で実行オブジェクトにそのまま埋め込まれる。つまり実行時には明確にメモリー上に実態がある。文字列リテラルというのはそのメモリーの別名、lvalue referenceだと捉えることもできるだろう。

しかし正確性を求めて、規格書リーディングをするのは本末転倒だし、名前の有無という圧倒的わかりやすさの前には、このくらいの例外は目をつぶっていいと思う。

アドレスを取得できるか否かで判別できるのではないか

lvalueはアドレスを取得できるがrvalueはアドレスを取得できない。

これは一見見分ける強力な手段に思える。しかし考えて欲しい。コードを書くときにいちいちアドレス演算子を書いてコンパイルしてみるなんて悠長なことをするだろうか、いいやしない。もっと脳内で簡単に判別できるものが必要だ。やはり名前の有無が一番わかり易いだろう。

rvalueをxvalueとprvalueに分解して理解する必要はあるのか

ない。今日限りを持って記憶から抹消するべきだ。

xvalueとprvalueが何かについては
Value categories - cppreference.com
によくまとまっている。

rvalueの寿命が延長される条件の理解には必要やろ!という声があるかもしれないが、前述の通り、引数以外にrvalue referenceを使うべきではないので全く持って必要ない。

なんで理解する必要がないかについては
lvalueとrvalueとmoveと - Togetterまとめ
を参照して欲しい。コードを書くのに一ミリも寄与しないどころか混乱するだけで、有害であるとはっきりわかる。

まとめ

lvalueとrvalueの違いは

  • 名前があればlvalue、なければrvalue
  • ただしlvalue referenceは常にlvalue

でとりあえずはいいと思う。

License

CC BY 4.0

CC-BY icon.svg

97
70
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
97
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?