🔧

Assertで「0 &&」ってつけるのめんどくさい

2023/08/12に公開

はじめに

(C++20で追加されたクラスを使うので、C++20以降でしか動きません
C++のバージョンの変え方が分からない人は「C++ 開発環境名(VisualStudioなど) バージョン変更」と調べてみてください。)

この記事ではC++でプログラムをしていて、Assertを使用する際「0 &&」と入力するのが面倒だと感じた私が 「0 &&」を書かなくても簡単に呼び出せるAssert関数を作ったので記事にしてみました。
その関数だけ見たい方は 記事の下の方まで飛ばしてください
まだまだプログラミング勉強中なので間違った知識を記事にしてしまっていたり、もっと良い書き方があれば修正しますので教えていただけると幸いです。
(優しく教えてくださいm(__)m)
また、この記事は自分がプログラムを初めて1年目の頃でも理解できるような記事を目標に作成したので当たり前のことが書かれているかもしれませんが気にしないでください。

(前提)Assertとは

知っている方はとばしてください。
知らない方は自分で調べてもらってもOKですが、簡単にここにまとめておきます。

C++のAssertは、エラー箇所で自動的に実行を停止することができるデバッグ用の機能です。
使用するためには 「cassert」 をインクルードします。
例として、以下のプログラムをご覧ください

Assert 例
#include<cassert>

//Create関数はなんらかのオブジェクトを作成する処理ということで
//初期化成功でtrue・失敗でfalseがかえってくるものとする
 bool result = Create();
//assertマクロは、引数がfalseの時にプログラムの実行を停止してくれる
 assert(result);
 
//以下の書き方では、Releaseビルド時にCreate関数が実行されないので注意
 assert(Create());

作成関数が成功すればassertマクロは何もしません。
しかし、仮に作成関数が失敗し、resultにfalseが入った時にAssertマクロがメッセージボックスを出して、プログラムを終了させてくれるというものです。
出力にassertが実行されたファイル名と行数が出力されます。
また、Debugビルド時には動作しReleaseビルド時には実行されないです。
(Release時はassertの中に関数を書いた場合それすら実行されないので注意
基本的にassertの中に関数をそのまま入れるのは非推奨。bool型変数などを間に挟もう)

後述する_wassertとは違い処理が実行されないだけなのでコンパイルエラーになったりはしません)

基本的に「失敗するわけがない」ものに確認作業として使うものだと思ってください。
(↑これ本当に大事)

また、assertに任意の文字列を出力する際は以下のような書き方で出力することができます。

Assert 任意の文字列出力 例
#include<cassert>

if(!Create()) //if(Create()==false)と同じ
{
    assert(0 && "初期化が失敗したよ");
}

こうすることで、assertには「[0(偽)]かつ[文字列(これは真)]」が入り、falseになるのでassertが実行されるという書き方です。
(エラー出力に「0 && [エラー文字列]」と表示されるのでちょっとダサいが仕方がない...)

本題の「0 &&」うつのめんどくさい件

1つ上のソース例のassertにて「0 &&」と入力してから任意の文字列を書かなければassertにfalseが入らないため、「0 &&」と入力しなければいけません。しかし、めんどくさいと感じました。

また、実行中に作成した文字列を入れることもできないようになっています。
(以下、ありえないソースだが例なので許してください)

Assert 動的な文字列の出力 失敗例
//文字列を使用する際は「string」のインクルードが必要
#include<string>

//エラー時に出力する文字列
std::string errorMsg;

if(!Create())
{
    errorMsg = "Createで失敗したね";
}
if(!CreateOther())
{
    errorMsg = "なんか失敗したね";
}

//std::string::empty()・・・文字列が空かどうかを判定する
if(!errorMsg.empty())//エラー文字列が入っていたら通る
{
    assert(0 && errorMsg.c_str());
}

実行はされるが、出力には「失敗したね」ではなく「errorMsg.c_str()」と表示されてしまう。文字列ではなく、ソースコードの内容がそのまま展開されてしまうのです。

_wassertを使う

動的に作成した文字列をassertで使用するためにはassertではなく 「_wassert」 を使用する必要があります。
使い方の例

_wassert 例
//今回はエラーが起きたものとする
std::wstring errorMsg = L"エラーでした!!"; //stringではなくwstringなので注意!
#ifdef _DEBUG
_wassert(errorMsg.c_str(),_CRT_WIDE(__FILE__),(unsigned int)__LINE__);
#endif // DEBUG

_wassertの超簡潔説明

  1. 引数3ついります
  2. 第一引数にはエラーメッセージ「ワイド文字列」 で必要。「"もじ"」のような書き方ではワイド文字列ではなくただの文字列なので、「""」の前に 「L」 を書くことでワイド文字列になります 「L"わいどもじ"」
  3. 第二引数にはファイル名が入り、それを「ワイド文字列」に変換して渡します。「__FILE__」 マクロで__FILE__を書いたファイル名を取得できます。_CRT_WIDEマクロでワイド文字列にしています。
  4. 第三引数には行数が入る。 「__LINE__」 マクロで__LINE__を書いた行数を取得できる 。キャストは念のため。
  5. Releaseビルドでは_wassert関数は存在しないため、「#ifdef _DEBUG」で対応してあげる必要がある

使い方的には以上です。
注意として、_CRT_WIDEマクロは文字列をワイド文字列に変換してくれるものではないです。文字列リテラルの先頭に「L」を追加しているだけなので変換マクロではないです。
「_CRT_WIDE(errorMsg.c_str())」とすると「LerrorMsg.c_str()」とコンパイルされるのでコンパイルエラーになります。注意してください。

問題点

しかし、このままでは2点ほど問題があります。

  1. 毎回「_wassert(「エラーメッセージ」,_CRT_WIDE(__FILE__),(unsigned int)__LINE__);」って書くんなら「0 &&」の方が楽じゃん。意味ないじゃん。
  2. プロジェクト内でこの部分は文字列、この部分はワイド文字列、この部分は文字列・・・となっているとややこしいためワイド文字列ではなく文字列を扱いたい。(L書くのも面倒だしね)

この2つを解決させるために自前で関数を作ります。

「0 &&」入力しなくていい関数作成

自前で関数を作る(ErrorAssert関数)まだまだ未完成
#include<cassert>
#include<string>

//C++17以降の機能 std::string_viewを使うときに必要
#include<string_view>

//以下はヘッダーファイルに
void ErrorAssert(std::string_view errMsg);

//以下はcppファイルに
void ErrorAssert(std::string_view errMsg)
{
#ifdef _DEBUG
    //errMsgをワイド文字列に変換する処理
    std::wstring wErrMsg= /*ここに変換する関数*/
    //====ここまで変換処理

   
    _wassert(wErrMsg.data(),_CRT_WIDE(__FILE__),(unsigned int)__LINE__);
     //data()はc_str()でも動く。表現がCかC++かの違いです
#endif // DEBUG
}

文字列をワイド文字列に変換するにあたって、自作していただいても構いませんが世の中の神様のような人がすでに変換関数を作ってくれてます。今回はありがたく使用させていただきます。
(あくまで他の方が作ったソースなので、自分が作ったソースとディレクトリの場所を分けることを推奨します。)
https://github.com/javacommons/strconv

「strconv.h」のみを使いますので、使う場所にコピぺしていただいたらインクルードしておいてください。そしてありがたく関数を使わせていただきます。

自前で関数を作る(ErrorAssert関数)まだ未完成
//strconv.hをインクルードしておく

void ErrorAssert(std::string_view errMsg)
{
#ifdef _DEBUG
    //errMsgをワイド文字列に変換する処理
    std::wstring wErrMsg = sjis_to_wide(errMsg.data()); //これが神変換関数
    
    _wassert(wErrMsg.data(),_CRT_WIDE(__FILE__),(unsigned int)__LINE__);
#endif // DEBUG
}

一応これでも(停止するだけでいいなら)動きます。
しかし、ファイル名と行数がErrorAssert内の _wassertを書いたファイル名と行数になります。「__FILE__」マクロと「__LINE__」マクロはあくまでその行のファイル名と行数を取得できるだけなので、これではどこのファイルのどの行が原因でassertに引っかかっているのかがわかりません。
表示してほしいファイル名と行数はこのErrorAssert関数を呼んだ位置のファイル名と行数が欲しいです。ErrorAssert関数に引数を追加して「ErrorAssert(errMsg,__FILE__,__LINE__)」のようにすれば実装は可能ですがそれでは1の問題を解決できないので、関数を改良します。

自前で関数を作る(ErrorAssert関数)実はこれで完成
//C++20以降の機能 std::source_locationを使うときに必要
#include<source_location>

//以下はヘッダーファイルに
void ErrorAssert(std::string_view errMsg,
const std::source_location& location = std::source_location::current());

//以下はcppファイルに
void ErrorAssert(std::string_view errMsg,const std::source_location& location)
{
#ifdef _DEBUG
     //文字列をワイド文字列に変換する処理
    std::wstring wErrMsg = sjis_to_wide(errMsg.data());
    std::wstring wErrFile = sjis_to_wide(location.file_name());    

    _wassert(wErrMsg.data(), wErrFile.data(), (unsigned int)location.line());
#endif // DEBUG
}

あまり見ない型が出てきましたね。C++20で追加されたものです。
std::source_locationについての説明はここでは割愛させていただきますので、リファレンスを見ていただけるといいと思います。「__FILE__」や「__LINE__」の進化版みたいなものです。
(格納しておけるのがいい!)
https://cpprefjp.github.io/reference/source_location/source_location.html

第二引数はデフォルト引数を設定してあるので注意してください。

これで一応完成ですが、Releaseでビルドした際に「引数は関数の本体部で一度も参照されません」といった警告が出ます。
Debugビルド時には引数を関数内で使用していますが、Release時はifdefで囲った部分はコンパイルされない、つまりは無いものとされるので、引数を使用していないことが分かると思います。
今回の警告は動作には影響がありませんが、他の人(企業の人とか)が見た際に見栄えが悪いです。なので、消します。

ErrorAssert.cpp

//以下はcppファイルに
void ErrorAssert([[maybe_unused]] std::string_view errMsg,
		[[maybe_unused]] const std::source_location& location)
{
	//中身は一緒
}

変な括弧のやつが出てきました。
c++17から追加された機能で、意図的に未使用の要素を定義していることをコンパイラに伝え、警告を抑制するための 属性 です。

(この警告結構よく出るので、該当部分はこれを使って消しとこう👍)

https://cpprefjp.github.io/lang/cpp17/maybe_unused.html

完成版 ErrorAssert関数

この記事のまとめです。飛ばしてきた方、こちらです。
私はこの記事で作成した関数を(自作の)Utility.h・cppに書いています。以下にソースを書いておくので参考にしてください。
注意点
1.インクルードしておくべき標準ライブラリは 「cassert」「string」「string_view」「source_location」 です。

2.文字列からワイド文字列に変換する際に、以下のURLの 「strconv.h」 を使用させていただいています。こちらもインクルードを行う必要があります。変換をこれで行う方は、あくまで他の方のソースなのでディレクトリの場所を分けて使用することを推奨します。
https://github.com/javacommons/strconv
自前でワイド文字列に変換する方は変換する部分だけ自前のものを使用してください。

3.関数を見ていただければわかると思いますが、関数の第二引数はデフォルト引数でstd::source_location::current()にしています。こうすることで関数呼び出し元のファイル名や行数などの情報をとってきているので呼び出す際は第二引数には基本的に何も書かないでください
また、このこのクラス(std::source_location)がC++20以降の機能となりますのでご注意ください。
(std::string_viewはC++17以降)
([[maybe_unused]]属性もC++17以降)

Utility.h
//プリコンパイルヘッダ等を準備している方はそちらにinclude書いてください
#include<string>
#include<string_view>
#include<cassert>
#include<source_location>

//ワイド文字列に変換する神関数を使う人はこちらもinclude
//パスは各自調整してください
#include"strconv.h"

//assertを使う際"0 &&"と入力するのがめんどくさかったので作った関数
void ErrorAssert(std::string_view errMsg,
	const std::source_location& location = std::source_location::current());

Utility.cpp
#include "Utility.h"
void ErrorAssert([[maybe_unused]] std::string_view errMsg, [[maybe_unused]] const std::source_location& location)
{

//Releaseにはwassertはない!
#ifdef _DEBUG
	
    //==文字列をワイド文字列に変換する===
	std::wstring wErrStr = sjis_to_wide(errMsg.data());
	std::wstring wErrFile = sjis_to_wide(location.file_name());
    //===========ここまで==============  
																
	_wassert(wErrStr.data(), wErrFile.data(), (unsigned int)location.line());	
#endif // DEBUG
}

なお、呼び出しの際は以下のようにすれば呼び出せます。あら簡単。

呼び出し方
//例えば、何かを作成する関数Create関数が失敗したときfalseが帰ってくるものとして
bool result=Create();

if(!result)
{
    ErrorAssert("作成失敗しました");
}

また、WindowsAPIやDirectXなどの関数で「HRESULT型」が帰ってくるものには「FAILED」や「SUCCEEDED」などを使ってエラーを確かめると思いますが、上のソースのif文の部分をそのままHRESULT型にあてはめてあげると動作します。あら簡単。

HRESULTで呼び出し例
HRESULT result=/*HRESULT型が帰ってくる関数*/;

if(FAILED(result))
{
    ErrorAssert("作成失敗しました");
}

おわりに

c++20や17とかの新しい機能をつかえばいろいろなことができそうですね。

2023年12月5日追記:[[maybe_unused]]属性関係追加
これで警告もなくなったね👍

引用

文字列をワイド文字列に変換する神関数を作ってくれた方
https://github.com/javacommons/strconv

本記事を書くにあたって非常に参考にさせていただいた記事
(実は私の先輩の記事でした。見に行ってください。)
https://qiita.com/mewmew_tea/items/eeb42fa8d0cd10f63f9b

std::source_locationのリファレンス
https://cpprefjp.github.io/reference/source_location/source_location.html

[[maybe_unused]]属性のリファレンス
https://cpprefjp.github.io/lang/cpp17/maybe_unused.html

神戸電子専門学校ゲーム技術研究部

Discussion