C++

C++メタプログラミングでmemcpyを少し安全にする

投稿日:

背景

筆者はC++11を使用して開発を行っています。
開発していると、危険なlibcの関数をラップしてUtility関数として外に定義したい衝動に駆られます。その最たるものがmemcpyです。
memcpyに潜む危険性は(おそらくこの記事を読んでいる方には釈迦に説法だとは思いますが)以下が大きなものでしょうか。

  • 書き込み先バッファと読み込み先バッファがオーバーラップしていた場合
  • 書き込み先バッファのサイズが、読み込みサイズより足りない場合

実際にmemcpyを使用しているとき、問題になるのは二つ目かと思います。
SEGVでプログラムが死んでくれるならまだよいのですが、運が悪いと、メモリを破壊したまま動作を続けてしまって、思わぬ部分の動作がおかしくなります。
(ちなみに筆者の場合、マルチプロセスのミドルウェアの開発中にmemcpyによりセマフォの値を破壊していたことがあります。。。)

そこで、今回は、書き込み先バッファと読み込み先バッファのサイズを静的、つまりコンパイル時に観測し、必要に応じてコンパイルエラーが起こせるようなmemcpyラッパ関数を作成しようと思います。

環境

  • gcc version 5.4.0 20160609
  • Ubuntu 16.04

要件と仕様

要件は以下とします。

  1. memcpyを行う際、書き込み先バッファのサイズが読み込みサイズより小さい場合は、コンパイル時にその旨を開発者に通知する
  2. char型配列やint8_t/uint8_tなどの配列や、構造体のmemcpyを必ずサポートする

仕様は以下とします。

  1. 関数テンプレートを用いて、コンパイル時に書き込み先バッファと読み込み先バッファのサイズを取得し、static_assertを用いて適切にコンパイルエラーを起こす。
  2. サポートする変数は以下とする
    1. 組み込み型のうち、ポインタ型以外
    2. ユーザ定義型のうち、ポインタ型以外
  3. ポインタ型が関数に渡された場合はコンパイルエラーを起こす。

ポインタ型をサポートしないのは、ポインタ型の指す実体のサイズを取得することが困難なためです。

設計・実装

今回は関数テンプレートとconstexpr(定数式)、そしてtype_traits(型特性)を用います。
type_traitsは、C++11から標準ライブラリに追加されました。これを使用することにより、テンプレートで渡された型がどのような型かを判別することができます。

例えば、type_traitsのなかにある関数として、std::is_pointer<T>があります。この関数は、型Tがポインタ型の場合、コンパイル時にstd::true_typeに、そうでない場合std::false_typeになります。~_type型はメンバvalueに、bool型の定数を保持しています。
よって、std::is_pointer<T>::valueと書くと、T型がポインタならtrue、そうでないならfalseを返すメタ文になるわけです。

作成したサンプルコードは以下になります。

#include <iostream>
#include <type_traits>
#include <string.h>

template<typename T>
bool constexpr IsPointer(const T &arg) {  // ポインタ型かを判別するメタ関数
  return std::is_pointer<T>::value;
}

template<typename T, typename U>
void Memcpy(T &arg1, const U &arg2) {  // memcpyのラッパ関数
  static_assert(!IsPointer(arg1) && !IsPointer(arg2), "Pointer !");  // ポインタ型ならエラー
  static_assert(sizeof(T) >= sizeof(U), "write < read !");  // バッファ・サイズのエラー
  memcpy(&arg1, &arg2, sizeof(arg2));
}

typedef struct st {
  int *p;
} st;

int main() {
  int i1 = 0;
  int i2 = 0;
  char s1[4] = "foo";
  char s2[3] = {};
  st st1 = {};
  st st2 = {};

  Memcpy(i1, i2);  // 実体・実体
  Memcpy(s1, s2);  // 配列・配列
  Memcpy(s2, s1);  // 配列・配列、ただし書き込み<読み出し
  Memcpy(st1, st2);  // 実体・実体
  Memcpy(st1.p, st2.p);  // ポインタ・ポインタ

  return 0;
}

6行目に、引数の型がポインタ型かを判別するメタ関数IsPointerを定義しました。これはコンパイル時にbool型リテラルになります。
11行目に、memcpyのラッパ関数であるMemcpy関数を定義しました。この関数では、引数がポインタ型か、あるいはサイズが適切かどうかをstatic_assertによりコンパイル時にチェックします。引数が参照型なのは、ポインタ型として受け取るとその実体のサイズを判別することが難しいためです。

テスト

本プログラムのコンパイル結果は以下になります。

user@hostname:~/$ g++ main.cpp -std=c++11
main.cpp: In instantiation of ‘void Memcpy(T&, const U&) [with T = char [3]; U = char [4]]’:
main.cpp:31:16:   required from here
main.cpp:13:3: error: static assertion failed: write < read !
   static_assert(sizeof(T) >= sizeof(U), "write < read !");  // バッファ・サイズのエラー
   ^
main.cpp: In instantiation of ‘void Memcpy(T&, const U&) [with T = int*; U = int*]’:
main.cpp:33:22:   required from here
main.cpp:12:3: error: static assertion failed: Pointer !
   static_assert(!IsPointer(arg1) && !IsPointer(arg2), "Pointer !");  // ポインタ型ならエラー
   ^

ただしくポインタ型、バッファオーバーフローを検知できていることが確認できます。

まとめ

type_traitsには、この他にもさまざまなメタ関数が定義されています。また、SFINAEを利用することで、メタプログラミングの幅が更に広がることでしょう(SFINAEを利用したメタプロはいずれ紹介することになるかもしれません)。

メタプログラミングの最大の利点について、筆者は、実行時のリスクをコンパイル時に静的に検出できる点にあると考えます。これは開発者にとっては非常に有用で、とくにAPIを提供する場合やフレームワークを作成する場合などで輝くものだと思います(まさに現在の筆者の仕事です)。

ただし、メタプログラミングは(C++標準ライブラリの充実とともに改善されてはいますが)可読性が悪い、コンパイルエラーがわかりにくい等の欠点があります。ただし、これらは開発者集団がメタプログラミングに慣れていけば自然と改善するものと考えます。

積極的にメタプログラミングを行い、自分の組織に浸透させていければ、組織の開発レベルが向上するものと信じて、筆者もメタプログラミングを継続して学んでいきたいと思います。

-C++
-

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

関連記事

ltraceを用いてstd::vector::push_backの動作を見える化する

仕事で使用したltraceの使い方を、メモがてら残しておきます。 目次1 概要1.1 環境2 設計・実装3 ltraceを用いたdynamic libraryのトレース4 終わりに5 メモ5.0.1 …

C++のマルチスレッド環境における問題とvalgrindを用いた検出方法

私は仕事でC++を使用してソフトウェアを実装しています。 今回は、C++マルチスレッドプログラミングで発生する問題点とその検出方法を(自分のメモも兼ねて)記述します。 環境は以下になっております。 O …

C++11で作るTypeList(型情報コンテナ)とfor_each

目次1 経緯2 要件と仕様3 設計と実装3.1 LokiのTypeList3.2 C++11時代のTypeList3.3 TypeListへのアクセッサ(リストのサイズ・添字アクセス・型からの添字抽出 …

pthread_cancelすると何が行われるのか

仕事でのメモ。 目次1 pthread_cancel2 pthread_cancelで何が行われるのか2.1 シグナル送出2.2 注意2.3 ソースコードを読む2.4 pthread_cancel時の …

vimからC++プロジェクトに対してCMakeでビルドツリー生成+コンパイル

筆者はテキストエディタとしてvimを日常的に使用しています。 今回は、C++のソースツリーに対して、vim越しにCMakeコマンドを使用してビルドツリーを作成し、そのビルドツリー上でコンパイルを行うv …