コンパイルエラーの自動テスト

関連エントリ: ムーブによるpop


昨日の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!

のように、コンパイルエラーになる式をコメントアウトして示し、実際にコンパイルエラーになるかどうかはコメントアウトを外さないとわからない、という状況を改善できます。