[C++]フォーマット指定が不要なfprintf関数のような何か。

いちいちフォーマット指定しなくても、適当にフォーマットを生成してそれっぽく出力してくれるPrint関数を作った。……すげぇしょうもないもんを作ってしまった。

いや、その、訳あってFILE*とstreamを使い分ける必要が生じたのだ。特にフォーマットらしいフォーマットは必要なくて、与えられた値を素直に(スペースなど何らかの文字で区切って)出力すればいいだけ。このとき、stream相手なら<<演算子を繰り返し呼ぶだけで済むのに、fprintfだといちいちフォーマット指定をする必要があって、単純に呼ぶことができない。 こういうとき普通はstd::stringstreamなどを介して違いを吸収するのだが、如何せんstringやstringstreamは遅いので、パフォーマンスを気にする場合は使えない。今回はまさに1億回くらいの書き込みを行う場合の速度を改善するための試みだったので、もっと効率の良い方法を探さなければならなかった。

で、作ったのが次のような関数である。重要なのは下の方にある"Print"という関数。上の方にだらだらと書かれているのはその実装だ。

#include <type_traits>
#include <iostream>
#include <array>

namespace detail
{

template <class T>
struct GetArraySize_impl;
template <class Type, size_t N>
struct GetArraySize_impl<std::array<Type, N>> { static constexpr size_t Size = N; };

template <class Array>
constexpr size_t GetArraySize() { return GetArraySize_impl<std::decay_t<Array>>::Size; }

template <bool Rval>
struct Forward_impl
{
    template <class Type>
    static constexpr std::remove_reference_t<Type>& apply(Type&& t) { return static_cast<std::remove_reference_t<Type>&>(t); }
};
template <>
struct Forward_impl<true>
{
    template <class Type>
    static constexpr std::remove_reference_t<Type>&& apply(Type&& t) { return static_cast<std::remove_reference_t<Type>&&>(t); }
};

template <bool Rval, class Type>
constexpr decltype(auto) Forward(Type&& v) { return Forward_impl<Rval>::apply(std::forward<Type>(v)); }

template <class Array1, size_t ...Indices1, class Array2, size_t ...Indices2>
constexpr auto CatArray_impl(Array1&& a, std::index_sequence<Indices1...>,
                             Array2&& b, std::index_sequence<Indices2...>)
{
    constexpr size_t N1 = detail::GetArraySize<Array1>();
    constexpr size_t N2 = detail::GetArraySize<Array2>();
    using Type = typename std::decay_t<Array1>::value_type;
    static_assert(std::is_same<Type, typename std::decay_t<Array2>::value_type>::value, "");
    constexpr bool L1 = std::is_lvalue_reference<Array1>::value;
    constexpr bool L2 = std::is_lvalue_reference<Array2>::value;
    return std::array<Type, N1 + N2>{ Forward<!L1>(a[Indices1])..., Forward<!L2>(b[Indices2])... };
}

template <class Array>
constexpr Array CatArray(Array&& a)
{
    return a;
}
template <class Array1, class Array2>
constexpr auto CatArray(Array1&& a, Array2&& b)
{
    constexpr size_t N1 = detail::GetArraySize<Array1>();
    constexpr size_t N2 = detail::GetArraySize<Array2>();
    return detail::CatArray_impl(std::forward<Array1>(a), std::make_index_sequence<N1>(),
                                 std::forward<Array2>(b), std::make_index_sequence<N2>());
}

template <class Array1, class Array2>
constexpr auto CatArray_rec(Array1&& a, Array2&& b)
{
    return CatArray(std::forward<Array1>(a), std::forward<Array2>(b));
}
template <class Array1, class Array2, class Array3, class ...Arrays>
constexpr auto CatArray_rec(Array1&& a, Array2&& b, Array3&& c, Arrays&& ...as)
{
    return CatArray_rec(CatArray(std::forward<Array1>(a), std::forward<Array2>(b)), std::forward<Array3>(c), std::forward<Arrays>(as)...);
}
template <class ...Array>
constexpr auto CatArray(Array&& ...a)
{
    return detail::CatArray_rec(std::forward<Array>(a)...);
}
template <class ...Args>
constexpr auto MakeArray(Args&& ...args)->
std::array<
    typename std::decay_t<
    typename std::common_type_t<Args...>>,
    sizeof...(Args)>
{
    return std::array<
        typename std::decay<
        typename std::common_type<Args...>::type>::type,
        sizeof...(Args)>{ std::forward<Args>(args)... };
}

template <class Derived, template <int...> class Base>
class IsBasedOn_XN
{
private:
    typedef char  Yes;
    typedef struct { char c[2]; } No;

    template <int ...N>
    static constexpr Yes check(const Base<N...>&);
    static constexpr No check(...);

    static const Derived& d;
public:
    static constexpr bool value = sizeof(check(d)) == sizeof(Yes);
};
template <class ...T>
struct CatTuple;
template <class ...T1>
struct CatTuple<std::tuple<T1...>>
{
    using Type = std::tuple<T1...>;
};
template <class ...T1, class ...T2>
struct CatTuple<std::tuple<T1...>, std::tuple<T2...>>
{
    using Type = std::tuple<T1..., T2...>;
};
template <class T1, class T2, class T3, class ...Ts>
struct CatTuple<T1, T2, T3, Ts...>
    : public CatTuple<typename CatTuple<T1, T2>::Type, T3, Ts...>
{};
template <class ...T>
using CatTupleT = typename CatTuple<T...>::Type;

template <size_t Index, class ...Types>
struct GetFrontTypes;
template <class ...Types>
struct GetFrontTypes<0, Types...>
{
    using Type = std::tuple<>;
};
template <size_t Index, class Head, class ...Types>
struct GetFrontTypes<Index, Head, Types...>
{
    using Type = CatTupleT<std::tuple<Head>, typename GetFrontTypes<Index - 1, Types...>::Type>;
};
template <size_t Index, class ...Types>
using GetFrontTypesT = typename GetFrontTypes<Index, Types...>::Type;

template <size_t N>
struct GetFrontArgs_impl
{
    template <class Head, class ...Args>
    static constexpr auto apply(Head&& head, Args&& ...args)
    {
        return std::tuple_cat(std::forward_as_tuple(std::forward<Head>(head)), GetFrontArgs_impl<N - 1>::apply(std::forward<Args>(args)...));
    }
};
template <>
struct GetFrontArgs_impl<0>
{
    template <class ...Args>
    static constexpr auto apply(Args&& ...)
    {
        return std::tuple<>();
    }
};
template <size_t Index, class ...Args>
static constexpr auto GetFrontArgs(Args&& ...args)
{
    return GetFrontArgs_impl<Index>::apply(std::forward<Args>(args)...);
}

namespace print
{

constexpr size_t GetCharLen(int del)
{
    return (char)(del >> 8) == '\0' ? 1 : (char)(del >> 16) == '\0' ? 2 : (char)(del >> 24) == '\0' ? 3 : 4;
}
template <int ...Char>
struct Character
{
    template <size_t N>
    static constexpr int GetChar() { return std::get<N>(std::make_tuple(Char...)); }
    static constexpr auto Get()
    {
        return CatArray(Get_impl<Char>()...);
    }
private:
    template <int C>
    static constexpr auto Get_impl()
    {
        return Get_impl<C>(std::make_index_sequence<GetCharLen(C)>());
    }
    template <int C, size_t ...Indices>
    static constexpr auto Get_impl(std::index_sequence<Indices...>)
    {
        constexpr size_t Len = GetCharLen(C);
        return std::array<char, Len>{ char(C >> (8 * (Len - Indices - 1)))... };
    }
};
template <>
struct Character<>
{
    static constexpr auto Get()
    {
        return std::array<char, 0>();
    }
};
template <int ...Del> struct Delimiter : public Character<Del...> {};
template <int ...E> struct End : public Character<E...> {};

struct Flush {};

}

template <class Type_>
struct GetFmtSpec_ { static constexpr bool apply() { return true; } };
template <class Type>
struct GetFmtSpec_<Type*> { static constexpr auto apply() { return std::array<char, 2>{ '%', 'p' }; } };

#define GET_FMT_SPEC(TYPE, ...)\
template <> constexpr auto GetFmtSpec<TYPE>() { return MakeArray(__VA_ARGS__); }
template <class Type>
constexpr auto GetFmtSpec() { return GetFmtSpec_<Type>::apply(); }
GET_FMT_SPEC(float, '%', 'f');
GET_FMT_SPEC(double, '%', 'l', 'f');
GET_FMT_SPEC(long double, '%', 'l', 'f');
GET_FMT_SPEC(char, '%', 'c');
GET_FMT_SPEC(short, '%', 'h', 'd');
GET_FMT_SPEC(int, '%', 'd');
GET_FMT_SPEC(long, '%', 'ld');
GET_FMT_SPEC(long long, '%', 'l', 'l', 'd');
GET_FMT_SPEC(unsigned short, '%', 'h', 'u');
GET_FMT_SPEC(unsigned int, '%', 'u');
GET_FMT_SPEC(unsigned long, '%', 'l', 'u');
GET_FMT_SPEC(unsigned long long, '%', 'l', 'l', 'u');
GET_FMT_SPEC(const char*, '%', 's');
GET_FMT_SPEC(char*, '%', 's');
#undef GET_FMT_SPEC

template <class Head, class ...Args, std::enable_if_t<IsBasedOn_XN<Head, print::Delimiter>::value, std::nullptr_t> = nullptr>
constexpr auto GetOptions();
template <class Head, class ...Args, std::enable_if_t<IsBasedOn_XN<Head, print::End>::value, std::nullptr_t> = nullptr>
constexpr auto GetOptions();
template <class Head, class ...Args, std::enable_if_t<std::is_same<Head, print::Flush>::value, std::nullptr_t> = nullptr>
constexpr auto GetOptions();
template <class Head, class ...Args, typename decltype(GetFmtSpec<Head>())::value_type = '\0'>
constexpr auto GetOptions();
template <class ...Args, std::enable_if_t<sizeof...(Args) == 0, std::nullptr_t> = nullptr>
constexpr auto GetOptions()
{
    return std::make_tuple(print::Delimiter<' '>(), print::End<'\n'>(), false, 0);
}
template <class Head, class ...Args, std::enable_if_t<IsBasedOn_XN<Head, print::Delimiter>::value, std::nullptr_t>>
constexpr auto GetOptions()
{
    auto t = GetOptions<Args...>();
    return std::make_tuple(Head(), std::get<1>(t), std::get<2>(t), std::get<3>(t));
}
template <class Head, class ...Args, std::enable_if_t<IsBasedOn_XN<Head, print::End>::value, std::nullptr_t>>
constexpr auto GetOptions()
{
    auto t = GetOptions<Args...>();
    return std::make_tuple(std::get<0>(t), Head(), std::get<2>(t), std::get<3>(t));
}
template <class Head, class ...Args, std::enable_if_t<std::is_same<Head, print::Flush>::value, std::nullptr_t>>
constexpr auto GetOptions()
{
    auto t = GetOptions<Args...>();
    return std::make_tuple(std::get<0>(t), std::get<1>(t), true, std::get<3>(t));
}
template <class Head, class ...Args, typename decltype(GetFmtSpec<Head>())::value_type>
constexpr auto GetOptions()
{
    auto t = GetOptions<Args...>();
    return std::make_tuple(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t) + 1);
}

template <bool F>
void Flush(FILE* fp) { fflush(fp); }
template <>
void Flush<false>(FILE*) {}
template <bool F>
void Flush(std::ostream& ost) { ost << std::flush; }
template <>
void Flush<false>(std::ostream&) {}

template <class Del, class End>
constexpr auto MakeFormatStr_rec()
{
    return CatArray(End::Get(), std::array<char, 1>{ '\0' });
}
template <class Del, class End, class Head, class ...Args, typename decltype(GetFmtSpec<Head>())::value_type = '\0'>
constexpr auto MakeFormatStr_rec()
{
    auto t = MakeFormatStr_rec<Del, End, Args...>();
    return CatArray(Del::Get(), GetFmtSpec<Head>(), t);
}

template <class Del, class End, class Args>
struct MakeFormatStr;
template <class Del, class End, class Head, class ...Args>
struct MakeFormatStr<Del, End, std::tuple<Head, Args...>>
{
    static constexpr auto apply()
    {
        auto t = MakeFormatStr_rec<Del, End, Args...>();
        return CatArray(GetFmtSpec<Head>(), t);
    }
};

struct PrintStream
{
    template <class Del, class End, class Fls, class Head>
    void operator()(std::ostream& ost, Del, End, Fls, Head&& head) const
    {
        ost << std::forward<Head>(head) << CatArray(End::Get(), std::array<char, 1>{ '\0' }).data();
        Flush<Fls::value>(ost);
    }
    template <class Del, class End, class Fls, class Head, class ...Args, std::enable_if_t<sizeof...(Args) != 0, std::nullptr_t> = nullptr>
    void operator()(std::ostream& ost, Del d, End e, Fls f, Head&& head, Args&& ...args) const
    {
        ost << std::forward<Head>(head) << CatArray(Del::Get(), std::array<char, 1>{ '\0' }).data();
        return (*this)(ost, d, e, f, std::forward<Args>(args)...);
    }
};

}

namespace print
{
template <int ...N>
constexpr detail::print::Delimiter<N...> delim() { return detail::print::Delimiter<N...>(); };
template <int ...N>
constexpr detail::print::End<N...> end() { return detail::print::End<N...>(); };
constexpr detail::print::Flush flush;
}

template <class ...Args>
void Print(FILE* fp, Args&& ...args)
{
    constexpr auto t = detail::GetOptions<std::decay_t<Args>...>();
    constexpr auto d = std::get<0>(t);
    constexpr auto e = std::get<1>(t);
    constexpr bool f = std::get<2>(t);
    constexpr int n = std::get<3>(t);
    constexpr auto fmt = detail::MakeFormatStr<decltype(d), decltype(e), detail::GetFrontTypesT<n, std::decay_t<Args>...>>::apply();
    std::apply(&fprintf, std::tuple_cat(std::make_tuple(fp), std::make_tuple(fmt.data()), detail::GetFrontArgs<n>(std::forward<Args>(args)...)));
    detail::Flush<f>(fp);
}
template <class ...Args>
void Print(std::ostream& ost, Args&& ...args)
{
    constexpr auto t = detail::GetOptions<std::decay_t<Args>...>();
    constexpr auto d = std::get<0>(t);
    constexpr auto e = std::get<1>(t);
    constexpr bool f = std::get<2>(t);
    constexpr int n = std::get<3>(t);
    std::apply(detail::PrintStream(), std::tuple_cat(std::forward_as_tuple(ost, d, e, std::bool_constant<f>()), detail::GetFrontArgs<n>(std::forward<Args>(args)...)));
}


int main()
{
    int i;
    int* p = &i;
    //デフォルトは半角スペース区切り、末尾に改行文字、flushは行わない。
    Print(stdout, 1, "2345", 67.89f, p);//1 2345 67.889999 00000029A2EFF444\n
    //デリミタ、末尾、flushはオプションで指定できる。いずれか変更したいものだけ引数に与えれば良い。
    Print(stdout, 1, "2345", 67.89f, p, print::delim<', '>(), print::end<'end\n'>(), print::flush);//1, 2345, 67.889999, 00000029A2EFF444end\n
    //ostreamに対しても全く同様に呼び出せる。
    Print(std::cout, 1, "2345", 67.89, p, print::delim<', '>(), print::end<'end\n'>(), print::flush);//1, 2345, 67.89, 00000029A2EFF444end\n
    return 0;
}

……想像の3倍くらいのコード量になってしまった。いや、単純にスペース区切り末尾改行で出力させる分には簡単だったのだが、折角だからデリミタや末尾文字、flushなどのオプションも受け取りつつフォーマット文字列をコンパイル時に生成させてやろうと思って色々弄っていたら一気にコードが膨らんじゃったのだ。
実はこの記事、最初に投稿したときはオプション非対応の小規模なコードを載せていたのだが、実際に使っていると末尾改行を消すオプションがどうしても必要になって、ほぼ全て1から書き直したものが上の300行ほどのコードなのである。こんな関数を作ろうと思い立ったことをちょっと後悔した。しょうもない機能に時間を費やしすぎだ。

デリミタと末尾文字はコンパイル時に指定されていなければならない関係で、テンプレートパラメータとして与えなければならない。なので文字列ではなく文字リテラルで指定する必要がある。5文字以上を与えたい場合は次のようにする。

print::delim<'abcd', 'e, '>()

オプションに上のように与えれば、abcde,がデリミタとして使用される。末尾文字も同様である。文字数制限はないが、いちいち4バイトごとに区切らなければならないのは大変だ(C++17のテンプレートユーザー定義文字列リテラルを使えばもうちょっとスッキリするとは思うが、種々の事情により行っていない)。コンパイル時の文字列処理方法をそろそろ整えてくれないかなぁ。