C++

C++の条件演算子のちょっと細かい話あれそれ

More than 1 year has passed since last update.

条件演算子(Conditional-operator)とは

次のような、条件となる式と、条件が真の時に評価される式、条件が偽の時に評価される式の 3 つをとる演算子のことです。

<条件式> ? <条件が真の時に評価される式> : <条件が偽の時に評価される式>

ほぼ唯一とも言える 3 つの式をとる演算子のため、三項演算子(Ternary-operator)とも呼ばれています。

基本的な使い方

教科書的でとてもつまらない例で申し訳ないですが、例えばこんな感じで使えます。

#include <iostream>

// is_twice の真偽によって value か value*2 を返す
int twice_if(bool is_twice, int value)
{
    return (is_twice ? value*2 : value);
}

int main()
{
    using namespace std;

    // 0~9までの値で、偶数の時に is_twice をtrueにして twice_if を呼ぶ
    for (int i=0; i<10; ++i)
    {
        cout << twice_if(i%2 == 0, i) << endl;
    }

    return 0;
}
実行結果
0
1
4
3
8
5
12
7
16
9

この時、関数 twice_ifif を使って次のように書き直すことが出来ます。

int twice_if(bool is_twice, int value)
{
    if (is_twice) return value *2;
    return value;
}

つまり if を使って書けるものが、少し限定ながら簡潔に書けるところに利点がある演算子という訳ですね。

演算子の優先度は低め

C++における条件演算子は優先度がかなり低く、

  • 代入演算子(=)よりは優先度が高いが、
  • 論理OR(||)よりは優先度が低い

となっています。

このため、

v = a==b || c!=d ? x : y

上記の式は

v = (((a==b) || (c!=d)) ? x : y)

これと同じ解釈になります。

右結合である

条件演算子は右結合です。
つまり条件演算子が並列で現れたら、後(右)にある式の方が優先的に結合していると解釈されます。

例えば、

a ? b : c ? d : e

は、

(a ? b : (c ? d : e))

と同じ解釈になります。

これ、ifで書くと丁度次のような感じになります。

if (a)
{
    return b;
}
else
{
    if (c)
    {
        return d;
    }
    else
    {
        return e;
    }
}

以上から、並べて書いてswitchっぽく処理させることもできます、知られざる案外便利な手。
ただし、入れ子は素直に括弧で括る必要があるので、可読性は下がります。

最終的に評価される型が定まる必要がある

条件演算子を通しても結果として得られる値は1つなので、最終的に得られる値の型が何らかの形で決まる必要があります。

同じ型の場合

とりあえず両方とも同じ型であれば特に問題は起こりません、安心です。

struct A {};
struct B {};

const bool b = true;
const auto t0 = (b ? 10 : 100);// OK:いずれもint
const auto t1 = (b ? "true" : "false");// OK:いずれもconst char*
const auto t2 = (b ? A{} : B{});// NG:AとBは違う型!

違う型の場合

同じ型でない場合、(詳細は仕様に突っ込むことになるので省きますが)コンパイラが頑張って共通で取り出せる型を探し、勝手にその型として評価することになります。

struct A {};
struct C : public A {};

const bool b = false;
const auto t3 = (b ? A{} : C{});// OK:Cは基底クラスのAとして評価される
const auto t4 = (b ? 10 : 100.f);// OK:floatとして評価される
const auto t5 = (b ? "sl" : std::string{"s"});
// ↑OK:暗黙的に変換可能なので、std::stringとして評価される

うーむ、分からなくもないけど、これはちょっと雲行きが怪しくなってきましたね…。

実は左辺値としても評価され得る

表面的な使い方だけ見ていると右辺にしかでてこなそうな条件演算子ですが、
真の時の式と偽の時の式がいずれも左辺値で同じ型だった場合、結果も左辺値として評価されます。
(「そもそも左辺値とは何ぞや」という話は端折ります、すみません)

#include <iostream>

int main()
{
    using namespace std;

    const bool b = false;

    int x =0, y =1;
    (b ? x : y) = 10;

    cout << "x=" << x << " y=" << y << endl;

    return 0;
}
実行結果
x=0 y=10

もちろん、構造体やクラスであったとしてもこのルールは同じなので、次のようなコードも可能です。

#include <iostream>
#include <vector>

int main()
{
    using namespace std;

    vector<int> l, r;

    for (int i=0; i<10; ++i)
    {
        // 偶数だったら l に、そうでないなら r に追加
        (i%2 == 0 ? l : r).emplace_back(i);
    }

    cout << "--l:" << endl;
    for (const auto& it : l)
    {
        cout << it << endl;
    }
    cout << "--r:" << endl;
    for (const auto& it : r)
    {
        cout << it << endl;
    }

    return 0;
}
実行結果
--l:
0
2
4
6
8
--r:
1
3
5
7
9

ちなみに、同じ型でもconstなどで修飾されている場合、修飾されている方に合わされます。

vector<int> l;
const vector<int> r;

(b ? l : r).emplace_back(1);
// NG:const vector<int>&として評価されるので、constでないメソッドは呼べない

どちらか片方の式しか評価されない

#include <iostream>
using namespace std;

void f()
{
    cout << "f" << endl;
}

void g()
{
    cout << "g" << endl;
}

int main()
{
    for (int i=0; i<10; ++i)
    {
        // 偶数だったら f を呼ぶ、そうでないなら g を呼ぶ
        (i%2 == 0 ? f() : g());
    }
    return 0;
}
実行結果
f
g
f
g
f
g
f
g
f
g

短絡評価…という訳ではないですが、評価されるのはどちらか片方の式だけです。

これは直観的でいいですね。

ちなみに、今回の例のように真の時の型と偽の型の時がvoidだった場合、結果もちゃんとvoidとして評価されるので、このように書いてもコンパイラから特に文句を言われません。

関数呼び分けにも使える

さて、ここまでの知識を応用すると、シグネチャが同じであれば関数であろうと条件演算子で呼び分けられることに気付きます。

#include <iostream>
using namespace std;

void f()
{
    cout << "f" << endl;
}

void g()
{
    cout << "g" << endl;
}

int main()
{
    for (int i=0; i<2; ++i)
    {
        (i==0 ? f : g)();
    }

    return 0;
}
実行結果
f
g

まだ普通のコードに見えました??

この書き方の本当の強力凶悪さは、引数があるときに遺憾なく発揮されます。

#include <iostream>
#include <string>
using namespace std;

void f(int x, string y)
{
    cout << "f:" << x << " " << y << endl;
}

void g(int x, string y)
{
    cout << "g:" << (x*2) << " " << y << endl;
}

int main()
{
    for (int i=0; i<2; ++i)
    {
        (i==0 ? f : g)(10, "str");
    }

    return 0;
}
実行結果
f:10 str
g:20 str

確かに関数呼び出しを1回だけしていることは分かるけど…。

例えば副作用がある関数の呼び分けが条件演算子1つで出来る、というのはちょっとしたホラーですね。
この、一歩間違うと地獄行きしそうなところが何とも…。

つまりメンバ関数呼び分けもできる

関数が呼び分けできるのだから、メンバ関数呼び分けが出来ない訳がない!
とか思うとこういうコードも残念ながら書けてしまう事に気が付きます。

#include <iostream>
#include <string>
using namespace std;

struct A
{
    void f(int x, string y)
    {
        v_ += x;
        cout << "f:" << v_ << " " << y << endl;
    }

    void g(int x, string y)
    {
        v_ += x*2;
        cout << "g:" << v_ << " " << y << endl;
    }

    int v_ =0;
};

int main()
{
    A a{};

    for (int i=0; i<5; ++i)
    {
        // 偶数だったらAのfを、そうでないならAのgを呼ぶ
        (a.*(i%2 ==0 ? &A::f : &A::g))(i, "str");
    }

    return 0;
}
実行結果
f:0 str
g:2 str
f:4 str
g:10 str
f:14 str

うっ…これは、ヤバい。

おわりに

以上、実際問題書きやすくなる箇所はあるけれど、型の事故など懸念がいくつかあるため、一般的にあまり推奨されない条件演算子の話でした。

個人的には短く書けるのは素直に見通しよくなることも多くメリットなので、型が同じであるなら使っても特に問題はないかなと思ってます。
特に右結合である事を利用して並べて書くのは、可読性もあり悪い選択ではないと思ってます。

なお

こんなコードでも、ちゃんとした(well-formedな)コードなんだなっていうのを忘れないであげてください。

…でも、知ってて実践で役に立つことは、先ずないと思いますけど。

#include <iostream>
#include <vector>
using namespace std;

class MyVector
{
public:
    void push_front(int v)
    {
        vec_.insert(vec_.begin(), v);
    }
    void push_back(int v)
    {
        vec_.push_back(v);
    }

    vector<int> vec_{};
};

int main()
{
    MyVector l, r;

    for (int i=0; i<10; ++i)
    {
        ((i%2 == 0 ? l : r).*(i%3 == 0 ? &MyVector::push_front : &MyVector::push_back))(i%5==0 ? i*2 : i);
    }

    cout << "--l" << endl;
    for (const auto& it : l.vec_)
    {
        cout << it << endl;
    }
    cout << "--r" << endl;
    for (const auto& it : r.vec_)
    {
        cout << it << endl;
    }

    return 0;
}
実行結果
--l
6
0
2
4
8
--r
9
3
1
10
7

リファレンス

参考にさせて頂いたページなど、感謝!

C++での三項演算子(?: 条件演算子)は左辺値として使える
条件演算子と左辺値の扱いの差
条件演算子(三項演算子)を可読性低いとか言わせない