ゆとりーなの日記

日記的な事を書いて行くと思はれる

volatile参照渡しとvolatileメンバ関数と戻り値にvolatileを付けることの意義?

コンパイラ先生は優秀で、NRVOという最適化をしてくれることがあります。この最適化を施してくれると次のようなコードにおいて、

#include <iostream>

class Hoge {
 public:
     Hoge() : val_(0) {
         std::cout << "コンストラクト" << std::endl;
     }
     Hoge(const Hoge &rhs) : val_(rhs.val_) {
         std::cout << "コピーコンストラクト" << std::endl;
     }
     ~Hoge() {
         std::cout << "デストラクト" << std::endl;
     }
     void setVal(int val) {
         val_ = val;
     }
     int val() const {
         return val_;
     }
 private:
     int val_;
};

Hoge createHoge() {
    Hoge hoge;
    hoge.setVal(256);
    return hoge;
}

int main() {
    Hoge hoge(createHoge());
    std::cout << "val = " << hoge.val() << std::endl;
    return 0;
}

出力:
コンストラクト
val = 256
デストラクト

となって、コピーコンストラクタの呼び出しが消されてしまいます。普段はこの最適化はうれしいことが多いのですが、時にはコピーコンストラクタをちゃんと呼んでほしい場合もあるでしょう。
そこで思い出されるのがvolatile修飾子の存在です。こいつを変数に付けると、最適化を抑止してくれます。そこでcreateHoge関数とmain関数内を次のように修正してみると、

Hoge createHoge() {
    Hoge hoge;
    hoge.setVal(256);
    return hoge;
}

int main() {
    volatile Hoge hoge(createHoge());
    std::cout << "val = " << hoge.val() << std::endl;
    return 0;
}

エラー:
error C2662: 'Hoge::setVal' : 'volatile Hoge' から 'Hoge &' へ 'this' ポインターを変換できません。変換で修飾子が失われます。
error C2558: class 'Hoge' : コピー コンストラクターが使用できないか、'explicit' として宣言されています。
'Hoge::val' : 'volatile Hoge' から 'const Hoge &' へ 'this' ポインターを変換できません。変換で修飾子が失われます。

という二つのエラーが出てきます。要するに変数がvolatile宣言されてるのに、volatileじゃないメンバ関数とかvolatileじゃない参照をとるコピーコンストラクタが呼ばれたよと怒られるわけです。そこで、Hogeクラスの方を次のように修正します。

class Hoge {
 public:
     Hoge() : val_(0) {
         std::cout << "コンストラクト" << std::endl;
     }
     Hoge(const volatile Hoge &rhs) : val_(rhs.val_) {
         std::cout << "最適化阻止コピーコンストラクト" << std::endl;
     }
     ~Hoge() {
         std::cout << "デストラクト" << std::endl;
     }
     Hoge &operator =(const Hoge &rhs) {
         val_ = rhs.val_;
         std::cout << "コピー" << std::endl;
     }
     void setVal(int val) volatile {
         val_ = val;
     }
     int val() const volatile {
         return val_;
     }
 private:
     int val_;
};

まず、setValメソッドをvolatileメンバ関数にして、コピーコンストラクタに、const volatile参照を受けるものを追加しました。ところがこの修正だけだと

error C2558: class 'Hoge' : コピー コンストラクターが使用できないか、'explicit' として宣言されています。

というエラーが出てしまいます。どうやらvolatile修飾子が付いた変数を生成関数で初期化する場合には、戻り値にもvolatileを付けてやる必要があるみたいです。

volatile Hoge createHoge() {
    Hoge hoge;
    hoge.setVal(256);
    return hoge;
}

これでひとまずエラーはなくなりました。以下出力結果です。

出力:
コンストラクト
最適化防止コピーコンストラクト
デストラクト
val = 256
デストラクト

ちゃんとコピーコンストラクタが呼ばれました。
補足ですが実はこれ、main関数の方のhogeにはvolatileを付けなくてもちゃんとコピーコンストラクタは呼ばれます。

int main() {
    Hoge hoge(createHoge());
    std::cout << "val = " << hoge.val() << std::endl;
    return 0;
}

出力:
コンストラクト
最適化防止コピーコンストラクト
デストラクト
val = 256
デストラクト

まあ当たり前と言えば当たり前なのですが、こうすると、volatileが付いているコピーコンストラクタと付いていないコピーコンストラクタを両方用意しないといけないということが分かりました。volatileが付いているコピーコンストラクタだけだと、volatileが付いていない変数へのコピーコンストラクタとしては使えないみたいです。コピーコンストラクタを2種類用意してしまうと警告が出てしまうので、ここでは警告をつぶすためにmain関数内の方もvolatile修飾子を付けることにしました。