コンパイルエラーの自動テスト
昨日のmove_pop()
は、キューの要素型T
が例外を投げないムーブコンストラクタを持っていない場合、その関数を呼べないようにしていました。その「呼べない」、つまり「呼ぶとコンパイルエラーになる」という挙動を自動テストできるようにしてみました。
まず、全体のコード:
#include <queue> #include <type_traits> namespace detail { template <class T, class Container> class movable_queue : public std::queue<T, Container> { using base = std::queue<T, Container>; public: T move_pop() { static_assert(std::is_nothrow_move_constructible<T>{}, "T must be nothrow move constructible"); T x = std::move(base::front()); base::pop(); return x; } }; template <class T, class Container> class non_movable_queue : public std::queue<T, Container> {}; } // namespace detail template <class T, class Container = std::deque<T>> using extended_queue = typename std::conditional< std::is_nothrow_move_constructible<T>::value, detail::movable_queue<T, Container>, detail::non_movable_queue<T, Container> >::type; #include <iostream> #include <memory> #include <string> struct X { X() = default; X(X&&) noexcept(false) {} }; struct is_move_popable_impl { template <class T> static auto check(T x) -> decltype( std::declval<T>().move_pop(), std::true_type()); static auto check(...) -> std::false_type; }; template <class T> struct is_move_popable : decltype(is_move_popable_impl::check(std::declval<T>())) {}; int main() { // non copyable, movable { extended_queue<std::unique_ptr<int>> que; que.push(std::unique_ptr<int>(new int(3))); que.push(std::unique_ptr<int>(new int(1))); que.push(std::unique_ptr<int>(new int(4))); while (!que.empty()) { std::unique_ptr<int> p = que.move_pop(); std::cout << *p << std::endl; } } // basic_string has nothrow move constructor { extended_queue<std::string> que; que.push("aaa"); que.push("bbb"); que.push("ccc"); while (!que.empty()) { std::string s = que.move_pop(); std::cout << s << std::endl; } } // X hasn't nothrow move constructor { extended_queue<X> que; que.push(X()); que.push(X()); que.push(X()); // while (!que.empty()) { // X x = que.move_pop(); // Error!!! // } static_assert( !is_move_popable<decltype(que)>{}, "extended_queue<X> can't use move_pop()"); } }
一番下の以下のコードが、move_pop()
を呼ぶとコンパイルエラーなるかどうかのテストです。
static_assert( !is_move_popable<decltype(que)>{}, "extended_queue<X> can't use move_pop()");
コンパイルエラーをテストできるようにするために、まずキューの設計自体に手を入れました。単なるstatic_assert
だと、その時点でコンパイルエラーになってしまうので、テスト目的としては、move_pop()
を=delete
定義したり、最初から定義しなかったり、SFINAEで弾いたりする方法が望ましいです。
今回は、型T
が例外を投げないムーブコンストラクタを持っているかどうかで、ベースとなるキューの型自体をまるっと切り替えるようにしました。例外を(略)持っていない場合は、std::queue
から派生しただけのnon_movable_queue
を使用します。このキューにはmove_pop()
は定義していません。例外を(略)持っている場合は、std::queue
から派生し、move_pop()
を実装しているnon_movable_queue
をキューの型として使用するようにしています。
この2つの型は、エイリアステンプレートの中でコンパイル時型切り替えのstd::conditional
メタ関数を使用して切り替えています。
これで、キューのmove_pop()
をテスト可能な形に直せました。
move_pop()
を呼び出すとコンパイルエラーになるかどうかは、SFINAEでチェックしています。is_move_popable
がそのコンパイルエラーチェックを行うメタ関数です。SFINAEは特定の式が適格かどうかを判定して関数オーバーロードできるので、それを利用して、
std::declval<T>().move_pop()
という式が成り立つかどうかを調べています。ここでのT
はキューの型です。
これに要素型を含むキューの型を渡すことで、move_pop()
を呼べるかどうかが判定できます。
これで、C++の言語レベルで、コンパイルエラーの自動テストができました。これで
// expression; // error!
のように、コンパイルエラーになる式をコメントアウトして示し、実際にコンパイルエラーになるかどうかはコメントアウトを外さないとわからない、という状況を改善できます。