この記事はADVENTARのC++ Advent Calender 2015の18日目の記事です。
ところで、当投稿はQiitaのC++ Advent Calendar 2015の記事ではありません。
実は、ADVENTARへ投稿を登録した当記事が思ったより膨らんだため、分割して一部をQiitaのC++カレンダーへ投稿させて頂いております。
#1.はじめに
私は、折角のC++をベターCとして長年使ってましたが、去年、今更ですがC++11規格の存在を知りC++の発展にびっくり。バリバリ使い始めて、多少は役に立ちそうな小技ツールをいくつか作ったので、その中から2つ、ご紹介させて頂きます。
もし、お役に立つことがありましたら、幸いです。
#2.まずは軽くstd::extent<>用マクロ
C++11で規格された多次元生配列の要素数を取ってくるstd::extent<>](http://www.cplusplus.com/reference/type_traits/extent/)は便利なのですが、ラムダ式の中などで参照になってしまうと使えません。参照を[std::remove_reference<>で外せば良いのですが、滅多矢鱈長くなるので嫌らしいです。
こんな時は、タイプ量を減らすためにマクロ化です。EXTENT()は1次元用、EXTENT_N()は多次元用です。
使い方は次節を参照下さい。早速ラムダ式の中で使ってます。
#include <type_traits>
#define EXTENT(dType) \
std::extent<typename std::remove_reference<decltype(dType)>::type, 0>::value
#define EXTENT_N(dType, dRank) \
std::extent<typename std::remove_reference<decltype(dType)>::type, dRank>::value
#3.本命、ストロング・ブレーク
多重ループやループの中のswitchからループ脱出に苦労された方はいませんか? そんなあなたに朗報(?)です。
C++11でサポートされたラムダ式は、return;で呼び出し元に戻ります。そして、ラムダ式は定義しているところから、そのまま直接呼び出すことができます。これを使ってラムダ式をその場実行し、内部の多重ループからreturn;すると、あら不思議、多重ループをブレークできます。
更に戻り値を返却できるので、最後まで回ったら0、ブレークしたら0以外を返却すれば、抜けたところでブレーク処理や非ブレーク処理を記述できます。
ポイントだけ書くと⇩こんな感じです。
if ( int ret = [&]()->int { {ループ{ブレークしたい時return x;} } return 0;}() )
{
ブレーク時の処理;
}
else
{
最後まで回った時の処理;
}
太文字部分がラムダ式です。ラムダ式を定義してそのまま実行し、その戻り値をret変数へ設定しています。
「ここでループ」の中で、return x;することによりループやswitchを全て中断し、xをretへ設定します。
このret変数は、then節内でも参照できます。(else節内でももちろん参照できますが、常に0なので意味ないです。)
マクロを使って下記のように書けるようにしてみました。
TRY(ret)
{
ループ
{
ブレークしたい時、BREAK(x); // ただし、xはint型でx!=0
}
}
BROKEN()
{
ブレーク時の処理; // retにBREAK(x)のxが設定されている。
}
NOT_BROKEN()
{
最後まで回った時の処理; // retは常に0
}
以下、ソースです。有効行はたった5行です。でもこの5行だけだと、確実にハマるのでコメントを付けてます。
// ***************************************************************************
// ストロング・ブレーク
// 使い方例①:
// TRY(ret) // 内部でint ret;と宣言されている。
// {
// while(...)
// {
// :
// while(...)
// {
// :
// switch(...)
// {
// :
// BREAK(1); // retに1が設定される。
// :
// }
// :
// }
// :
// }
// }
// BROKEN() // BREAK(x);した。(ただし、x!=0)
// {
// std::cout << "BROKEN(ret=" << ret << ")\n";
// }
// NOT_BROKEN() // ループを最後まで回った(もしくはBREAK(0);した。)
// {
// std::cout << "NOT_BROKEN\n";
// }
//
// 使い方例②(BROKENのみ):
// TRY(ret) // 内部でint ret;と宣言される。
// {
// :
// BREAK(2); // retに2が設定される。
// :
// }
// BROKEN() // BREAK(x);した。(ただし、x!=0)
// {
// std::cout << "BROKEN(ret=" << ret << ")\n";
// }
//
// 使い方例③(COMPLETEDのみ):
// TRY(ret) // 内部でint ret;と宣言される。
// {
// :
// BREAK(3); // retに3が設定される。
// :
// }
// COMPLETED() // ループを最後まで回った(もしくはBREAK(0);した。)
// {
// std::cout << "COMPLETED()\n";
// }
// ***************************************************************************
#pragma once
#define TRY(dVar) if (int dVar=[&]()->int {
#define BREAK(dValue) return dValue
#define BROKEN() return 0;}())
#define NOT_BROKEN() else
#define COMPLETED() return 0;}()){}else
以下使用例です。
#include <iostream>
using namespace std;
#include "strong_break.h"
#include "extent.h"
int main()
{
int const values[]={4,2,9,3,6};
// 単ループから脱出し、ブレークした時に処理
TRY(ret)
{
for (int i=0; i < EXTENT(values); ++i) {
if (values[i] == 3)
BREAK(i+1);
}
}
BROKEN()
{
std::cout << "BROKEN(ret=" << ret << ")\n";
}
// 単ループから脱出し、ブレークした時としなかった時の両方で処理
TRY(ret)
{
for (int i=0; i < EXTENT(values); ++i) {
if (values[i] == 10)
BREAK(i+1);
}
}
BROKEN()
{
std::cout << "BROKEN(ret=" << ret << ")\n";
}
NOT_BROKEN()
{
std::cout << "NOT_BROKEN(ret=" << ret << ")\n";
}
// 単ループから脱出し、最後まで実行した時(=ブレークしなかった時)に処理
TRY(ret)
{
for (int i=0; i < EXTENT(values); ++i) {
if (values[i] == 10)
BREAK(i+1);
}
}
COMPLETED()
{
std::cout << "COMPLETED(ret=" << ret << ")\n";
}
// 多重ループからの脱出
int const values2D[][2]={{4,0},{2,6},{9,7},{3,2},{6,1}};
TRY(ret)
{
for (int i=0; i < EXTENT_N(values2D, 0); ++i) {
for (int j=0; j < EXTENT_N(values2D, 1); ++j) {
if (values2D[i][j] == 6) {
BREAK(i+1);
}
}
}
}
BROKEN()
{
std::cout << "BROKEN(ret=" << ret << ")\n";
}
// ループ内のswitchからのループ脱出
TRY(ret)
{
for (int i=0; i < EXTENT(values); ++i) {
switch(values[i])
{
case 0:
break;
case 1:
break;
case 9:
BREAK(i+1);
}
}
}
BROKEN()
{
std::cout << "BROKEN(ret=" << ret << ")\n";
}
return 0;
}
C++11対応が必要です。MSVC 2015とMinGW 4.9.2の-std=c++11オプション付きで動作確認しています。
【閑話休題】
本当は、TRY {...} CATCH(int e) {...}
みたいに書けるようにしたかったのですが、私の頭ではいくら考えてもできそうになかったので諦めました。
TRY(int ret)
みたいに「戻り値の型」をユーザが定義するようにもできる筈ですが、使うのが難しくなりそう(boolへの暗黙の型変換が必須)な割にメリットが少ないのでこちらも断念してます。
#4.最後に
正直、かなり意表を突く実装と思います。
勝負ごとなら意表を突くのは良いことですが、プログラミングの世界では重々慎むべきことです。知らない人にとって、非常に迷惑な話ですから。
ですので、業務では使う前に関係者の了解を得ることをお薦めします。
プロジェクトの特性によっては使うべきでないケースもありますので。
因みに、これの簡易版についてteratailで意見を聞いてみたことがあるのですが、可読性の面から使わないとの回答が大半でした。その後見つけたのですが、stackoverflowの4年前のQAで直接ラムダ式を使う案について、同様な議論がされてました。回答も大差ありませんでした。
この時より自然な構文でかけるようにできたし、goto使うよりコードを綺麗に保てるので、私のプロジェクトでは使います。