何気なく使っている三項演算子の闇が実は深かったのでメモ
三項演算子を左辺値として使う
三項演算子は基本的に右辺値でしか使わないイメージですが、実は左辺値としても使えるようです。
左辺値なので値を代入できます。
#include <stdio.h>
int main(int argc, char const* argv[])
{
int i, j, b;
i = j = 0;
b = 0;
(b ? i : j) = 10;
printf("i = %d, j = %d\n", i, j);
i = j = 0;
b = 1;
(b ? i : j) = 10;
printf("i = %d, j = %d\n", i, j);
return 0;
}
$ g++ test2.cpp
$ ./a.out
i = 0, j = 10
i = 10, j = 0
なんか文法に違和感がありますがちゃんと動作しますね・・・。
補足1 : Cの場合はエラーが発生する?
Cの場合はエラーが発生しました、左辺値として定義されるのはC++だけなんでしょうか?
test2.c: In function ‘main’:
test2.c:10:17: error: lvalue required as left operand of assignment
(b ? i : j) = 10;
^
test2.c:17:17: error: lvalue required as left operand of assignment
(b ? i : j) = 10;
追記:
@yohhoy さんに補足1についてコメントいただきました。言語仕様で決まってるんですね・・・
条件演算子と左辺値の扱いの差 - yohhoyの日記
補足2 : アセンブラで見てみる
以下のfunc1とfunc2はアセンブラコード上では等価になるようです。
int i = 0, j = 0, s = 0;
void func1() {
(s ? i : j) = 10;
}
void func2() {
if (s) {
i = 10;
} else {
j = 10;
}
}
gcc -S test3.cpp
を実行
.file "test3.cpp"
.globl i
.bss
.align 4
.type i, @object
.size i, 4
i:
.zero 4
.globl j
.align 4
.type j, @object
.size j, 4
j:
.zero 4
.globl s
.align 4
.type s, @object
.size s, 4
s:
.zero 4
.text
.globl _Z5func1v
.type _Z5func1v, @function
_Z5func1v:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl s(%rip), %eax
testl %eax, %eax
je .L2
movl $10, i(%rip)
jmp .L1
.L2:
movl $10, j(%rip)
.L1:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size _Z5func1v, .-_Z5func1v
.globl _Z5func2v
.type _Z5func2v, @function
_Z5func2v:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl s(%rip), %eax
testl %eax, %eax
je .L5
movl $10, i(%rip)
jmp .L4
.L5:
movl $10, j(%rip)
.L4:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size _Z5func2v, .-_Z5func2v
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
補足3 : メンバ関数もそのまま呼べる
左辺値なのでドット演算子を使ってメンバ関数もちゃんと呼び出せるっぽいです
#include <iostream>
#include <vector>
void func(bool s) {
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};
(s ? a : b).push_back(100);
std::cout << __func__ << " : " << std::boolalpha << s << std::endl;
for (auto n : (s ? a : b)) {
std::cout << n << std::endl;
}
}
int main(int argc, char const* argv[])
{
func(true);
func(false);
return 0;
}
$ g++ -std=c++11 test4.cpp && ./a.out
func : true
1
2
3
100
func : false
4
5
6
100
闇が深い・・・。(でも意外と便利かもしれない・・・?)
追記:
左辺値なのでと微妙な表現だったので型をちゃんと見ました。
#include <iostream>
#include <vector>
#include <typeinfo>
void func(bool s) {
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};
std::cout << "a = " << typeid(a).name() << std::endl;
std::cout << "b = " << typeid(b).name() << std::endl;
std::cout << "(s ? a : b) = " << typeid((s ? a : b)).name() << std::endl;
}
void func2(bool s) {
std::cout << "(s ? rvalue : rvalue) = "
<< typeid((s ? std::vector<int>() : std::vector<int>())).name() << std::endl;
(s ? std::vector<int>() : std::vector<int>()).push_back(100);
}
int main(int argc, char const* argv[])
{
func(true);
func2(true);
return 0;
}
$ g++ -std=c++11 test4.cpp && ./a.out
a = St6vectorIiSaIiEE
b = St6vectorIiSaIiEE
(s ? a : b) = St6vectorIiSaIiEE
(s ? rvalue : rvalue) = St6vectorIiSaIiEE
今回の (s ? a : b)
は std::vector<int>
の型を返す式になるので、そのメンバ関数も呼び出せるということですね。
@yohhoy さんにコメントでご指摘いただいたように、右辺値でもメンバ関数は呼び出せるようです。
参考
今すぐ使える C++ コーディングテクニック集 - torus711 のアレ
C++ の、スマートだと思う人がいるかもしれないコードの書き方12選のつもりが6個ぐらいで断念 - Qiita
ちひろのページ - 左辺値に三項演算子