TL;DR
- C++20では、異なるenum同士などの演算はdeprecatedなので使わないでね!
- bad knowhowとして、enumの先頭に"+"をつけると、warningは出なくなる
- でも(気軽に)良い子は真似しないでね!容量用法を守って正しく使ってね!
自己紹介
- 元経歴10年超の組込系エンジニア。C/C++がメインでバリバリOSありなし関わらずお仕事してた
- いきなり全くの畑違いに左遷され、大変な事になっております ま、それはよいとして
OpenCVのC++20対応で問題になったenum演算
例えば、上位bitと下位bitを別のenumに定義して、ORedなどで使いたい事ってありますよね(昔のソースコードがそうなっていて困っている事の方が多いかな?)
#include <iostream>
enum {
TYPE_JPG = 0x10000000,
TYPE_PNG = 0x20000000,
};
enum {
JPG_BASE = 0x0000,
JPG_PROG = 0x0001,
};
int main()
{
auto mode = TYPE_JPG | JPG_BASE;
std::cout << mode << std::endl;
return 0;
}
このコードはGCCでもclang++でも、C++17でbuildしても怒られないが、C++20でbuildするとwarningが発生する。
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ clang++ main.cpp -o a.out --std=c++17
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ clang++ main.cpp -o a.out --std=c++20
main.cpp:15:24: warning: bitwise operation between different enumeration types ('(unnamed enum at main.cpp:3:1)' and '(unnamed enum at main.cpp:8:1)') is deprecated [-Wdeprecated-anon-enum-enum-conversion]
15 | auto mode = TYPE_JPG | JPG_BASE;
| ~~~~~~~~ ^ ~~~~~~~~
1 warning generated.
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ g++ main.cpp -o a.out --std=c++17
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ g++ main.cpp -o a.out --std=c++20
main.cpp: In function ‘int main()’:
main.cpp:15:24: warning: bitwise operation between different enumeration types ‘<unnamed enum>’ and ‘<unnamed enum>’ is deprecated [-Wdeprecated-enum-enum-conversion]
15 | auto mode = TYPE_JPG | JPG_BASE;
|
どうして怒られるのか?
三方比較演算子の導入にともない、一方のオペランドが列挙型である場合の算術演算での暗黙の算術変換を非推奨とする。
はい、ここ重要です。「暗黙の」算術変換を非推奨とする、とあります。
enum
v.s. 別のenum
、だけでなく、 enum
v.s. 整数、 enum
v.s. 浮動小数 など、なんでもエラーになります。
まじめにキャストしてもいいんだけどね
最初、ちゃんと整数型へキャストすることで、問題を回避しよう、と思いました。
でも、なんだかな、といまいち感が否めないのも否定しきれませんでした。
int main()
{
auto mode = static_cast<int>(TYPE_JPG) | static_cast<int>(JPG_BASE);
std::cout << mode << std::endl;
return 0;
}
たった1つの…ではないけど、冴えたやりかた
OpenCV communityにpull requestを出したところ「+をつければいいよ」とのアドバイス。ほほお!
int main()
{
- auto mode = TYPE_JPG | JPG_BASE;
+ auto mode = +TYPE_JPG | JPG_BASE;
std::cout << mode << std::endl;
return 0;
}
たったこれだけでございます!
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ g++ main.cpp -o a.out --std=c++17
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ g++ main.cpp -o a.out --std=c++20
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ clang++ main.cpp -o a.out --std=c++17
kmtr@kmtr-VMware-Virtual-Platform:~/work/qiita$ clang++ main.cpp -o a.out --std=c++20
そして後から見ると、cpprefjpさまにもこの記載がありましたね・・・
enum E1 { e };
enum E2 { f };
bool b = e <= 3.7; // C++20から非推奨
int k = f - e; // C++20から非推奨
int x = +f - e; // OK
その他の解決策も考える
こういうトリックをやると、警告を出していた意味が見えなくなるため、もう少しまともな対策も考える。
(回避策1):ORedしたい場合、1つの定義内にまとめる
ORedして使いたい列挙子は、1つの定義内にまとめれば、そもそも警告は出ない。こちらのほうが推奨かしら?ただし、どんどん肥大化し、意味でわけられなくなる、というデメリットもある。
enum {
TYPE_JPG = 0x10000000,
TYPE_PNG = 0x20000000,
JPG_BASE = 0x0000,
JPG_PROG = 0x0001,
};
(回避策2)整数などと一緒に計算する可能性がある定数定義にenumを使う場合、constexpr
への置き換えも考える
constexpr auto XY_ONE = 1 << 6;
int px = p.x / XY_ONE;
int px = p.y / XY_ONE;
だけどね・・・
OpenCV pullrequestにいれたら、やっぱり"+" trickは分かりにくいから、static_cast<>しようぜ!というレビューをもらってしまいました。あばばばばばばばばば(ここにきて頭が痛い)
ということで。
- 他enumとORedで使う場合は、cast先するべき型がそもそもないので、 + trickを許して!
- doubleやfloatと演算する場合は、static_cast<>(ENUM) するね!
というあたりを落としどころにしてみた。
(12/14 追記)力技ではない、正攻法
@yumetoda さんより、コメント欄で教えていただきましたので、こちらも紹介をさせていただきます。
enum classにして明示的にoperator overloadを書くという手もあると思いますね。
そう!こういう正攻法が使えれば良かったのですが・・・
- OpenCV projectの場合、"C++11でも、C++17でも、今後は、C++20/23/26でも、buildしてtestが正常に動作できるように!"ということが求められる。
- C++のソースコードからpython bindingを自動生成したりしており、defineを変更するのは難しいらしい(内部処理でAPI関わらない部分なんだけどなあ)
こうした「しがらみ」もあり、抜本的な対策が打てなかったのでございます。
まとめ
- C++20から、列挙子は他列挙子や数値と混ぜるな危険でございます!
- "+"をつけることで、ごまかせるみたい。でも分かりにくいから、レビューで怒られるかも!
- 正式対応も色々考えていきましょう!
以上です、ご精読、ありがとうございました!