Home / ぼやきごと / 2013-12-27
2013-12-27

C++: array クラスの要素数指定を省略し、コンパイル時に要素数チェックする

Prev: [C++:配列の要素数省略によるコンパイル時記述漏れチェック]

次のページを見てみてください。

このページで紹介されている make_array 関数と auto による型推論を用いれば、要素数を指定せずに std::array を生成することができます。

const auto values = make_array<int>(1, 2, 3, 4, 5);

なお、元記事の make_array はインライン関数ですが、次のようにすることで constexpr 関数にもできます。*1

#include <array>

template<typename T, typename ...Args>
constexpr std::array<T, sizeof...(Args)> make_array(Args&&... args)
{
    return std::array<T, sizeof...(Args)>{ static_cast<Args&&>(args)... };
}

これにより、T がリテラル型で、かつすべての引数が定数値の場合に限り、 constexprstd::array インスタンスを要素数指定無しで作成可能になります。

// auto は std::array<int, 5> になる。
constexpr auto values = make_array<int>(1, 2, 3, 4, 5);

そして、 std::array の要素数を得るメンバ関数 sizeconstexpr 関数です。
つまり、前回の記事に書いた、「std::array を使いつつ、組み込み配列のように要素数の変更をコンパイル時検出する」ことが次の記述で可能になります。

// constexpr std::size_t ReqCount = 5; がどこかのヘッダで定義されているものとする。

constexpr auto values = make_array<int>(1, 2, 3, 4, 5);
static_assert(values.size() == ReqCount, "values size error!");

ただし、この方法はT がリテラル型の場合に限ります。
なぜなら、そうでなければ std::array<T, N> もリテラル型にならず、メンバ関数 sizeconstexpr 関数にならないためです。

T がリテラル型以外の、例えば std::string 型の場合、別の記述方法をとる必要があります。
それは次のようなコードになります。

#include <array>
#include <type_traits>
#include <string>

// constexpr では受け取れないので const で受け取る。
// auto は std::array<std::string, 5> になる。
const auto values = make_array<std::string>("a", "b", "c", "d", "e");
static_assert(
    std::tuple_size<typename std::remove_cv<decltype(values)>::type>::value == ReqCount,
    "values size error!");

何やら長ったらしいですが、やっていることは std::tuple_size<std::array<T, N>>::value によって要素数をコンパイル時定数として取得しているだけです。

この方法は constexprstd::array であっても使えるので、こちらの方が汎用性の高いチェック方法であるといえます。

…とはいえ、「長ったらしくて書いてられん!」と思うことでしょう。
そこで、前回の記事で紹介した array_size 関数を std::array 用にオーバロードしてみます。*2

#include <array>

namespace util
{
    // std::array の要素数を取得する。
    template<typename T, std::size_t N>
    constexpr std::size_t array_size(const std::array<T, N>&) { return N; }

    // 固定長配列の要素数を取得する。(前回の記事で紹介した関数)
    template<typename T, std::size_t N>
    constexpr std::size_t array_size(T (&)[N]) { return N; }
}

この関数を使えば、 constexpr の有無や、型 T がリテラル型であるか否かに関わらず、次のように書くことができます。

#include <string>

constexpr auto values = make_array<int>(1, 2, 3, 4, 5);
static_assert(util::array_size(values) == ReqCount, "values size error!");

const auto texts = make_array<std::string>("a", "b", "c", "d", "e");
static_assert(util::array_size(texts) == ReqCount, "texts size error!");

// もちろん const 無しでもOK!
auto texts2 = make_array<std::string>("a", "b", "c", "d", "e");
static_assert(util::array_size(texts2) == ReqCount, "texts2 size error!");

ここまでできれば、もう std::array を使わない理由は無いですね。

なお、VC++2013でも、 make_arrayconstexpr 関数にせず、受け取る変数を constexpr にせず、 std::tuple_size を使って要素数をチェックする方法であればコンパイルすることができます。

Next: [C++: array クラスを利用した多次元配列型を定義してみた]
Category: [C++][プログラミング] - 2013-12-27 01:55:06

*1 C++14では std::forward が constexpr 関数となるため、元記事の inlineconstexpr に置き換えるだけでOKになります。
*2 こちらも2013年12月時点のVC++ではコンパイルできません。