テンプレートのインスタンス化と特殊化 解答ページ | Programming Place Plus 新C++編

トップページ新C++編テンプレートのインスタンス化と特殊化

このページの概要 🔗

このページは、練習問題の解答例や解説のページです。



解答・解説 🔗

問題1 (基本★★) 🔗

渡された値を出力するテンプレートを作成してください。明示的特殊化を使って、文字を出力するときは '' で、文字列を出力するときは "" で囲むようにします。クラステンプレートと関数テンプレートのそれぞれで実装してください。


まずクラステンプレートによる実装です。

最初にプライマリテンプレートを作成します((本編解説))。プライマリテンプレートは、明示的特殊化をおこなうベースにあるテンプレートで、いわばデフォルトで使われるテンプレートになります。

template <typename T>
class Printer {
public:
    void print(const T& value) const
    {
        std::cout << value << "\n";
    }
};

この時点では、p.print('x');p.print("xyz"); のように文字や文字列を渡しても、プライマリテンプレートが呼び出されるだけなので、xxyz が出力されるだけです。

次に文字版と文字列版のテンプレートを作成します。これらが明示的特殊化されたバージョンになります。

template <>
class Printer<char> {
public:
    void print(const char& value) const
    {
        std::cout << "'" << value << "'\n";
    }
};

template <>
class Printer<const char*> {
public:
    void print(const char* value) const
    {
        std::cout << "\"" << value << "\"\n";
    }
};

template <>
class Printer<std::string> {
public:
    void print(const std::string& value) const
    {
        std::cout << "\"" << value << "\"\n";
    }
};

文字版はテンプレート仮引数 T を char に置き換えたものを用意します。同様に文字列版として const char* と、std::string に置き換えたものを用意しました。

プログラム全体としては次のようになります。実行結果をみると、型ごとにテンプレートの使い分けができていることが分かります。

#include <iostream>
#include <string>

template <typename T>
class Printer {
public:
    void print(T value) const
    {
        std::cout << value << "\n";
    }
};

template <>
class Printer<char> {
public:
    void print(const char& value) const
    {
        std::cout << "'" << value << "'\n";
    }
};

template <>
class Printer<const char*> {
public:
    void print(const char* value) const
    {
        std::cout << "\"" << value << "\"\n";
    }
};

template <>
class Printer<std::string> {
public:
    void print(const std::string& value) const
    {
        std::cout << "\"" << value << "\"\n";
    }
};


int main()
{
    using namespace std::literals::string_literals;

    Printer<int> pi {};
    Printer<char> pc {};
    Printer<const char*> ps {};
    Printer<std::string> pss {};

    pi.print(123);
    pc.print('x');
    ps.print("abc");
    pss.print("abc"s);
}

実行結果:

123
'x'
"abc"
"abc"

同じことを関数テンプレートで実装すると次のようになります。単に出力する関数があればいいだけなので、こちらのほうが簡単です。

#include <iostream>
#include <string>

template <typename T>
void print(T value)
{
    std::cout << value << "\n";
}

template <>
void print(const char& value)
{
    std::cout << "'" << value << "'\n";
}

template <>
void print(const char* value)
{
    std::cout << "\"" << value << "\"\n";
}

template <>
void print(const std::string& value)
{
    std::cout << "\"" << value << "\"\n";
}


int main()
{
    using namespace std::literals::string_literals;

    print(123);
    print('x');
    print("abc");
    print("abc"s);
}

実行結果:

123
'x'
"abc"
"abc"

問題2 (基本★★) 🔗

任意のコンテナのすべての要素を出力する関数テンプレートを作成してください。そのコンテナの要素がポインタ型の場合、そのポインタが指し示す先にある値を出力します。ヌルポインタのときには “null” を出力してください。


たとえば次のように実現できます。

#include <array>
#include <iostream>
#include <deque>
#include <vector>


template <typename T>
void print_value(T value)
{
    std::cout << value;
}

template <typename T>
void print_value(T* value)
{
    if (value == nullptr) {
        std::cout << "null";
    }
    else {
        std::cout << *value;
    }
}


template <typename Container>
void print_container_values(const Container& c)
{
    auto it = std::cbegin(c);
    const auto it_end = std::cend(c);

    while (it != it_end) {
        print_value(*it);
        ++it;
        if (it != it_end) {
            std::cout << ", ";
        }
    }
    std::cout << "\n";
}


int main()
{
    std::vector<int> v {0, 1, 2, 3, 4};
    std::vector<int*> vp {&v[0], &v[1], nullptr, &v[3], &v[4]};
    print_container_values(v);
    print_container_values(vp);

    std::deque<int> d {0, 1, 2, 3, 4};
    std::deque<int*> dp {&d[0], &d[1], nullptr, &d[3], &d[4]};
    print_container_values(d);
    print_container_values(dp);

    std::array<int, 5> a {0, 1, 2, 3, 4};
    std::array<int*, 5> ap {&a[0], &a[1], nullptr, &a[3], &a[4]};
    print_container_values(a);
    print_container_values(ap);
}

実行結果:

0, 1, 2, 3, 4
0, 1, null, 3, 4
0, 1, 2, 3, 4
0, 1, null, 3, 4
0, 1, 2, 3, 4
0, 1, null, 3, 4

コンテナを受け取って要素を出力する print_container_values関数テンプレートを作成し、実際に要素を出力する部分を print_value関数テンプレートに実装しています。print_value関数テンプレートのほうを明示的特殊化することで、ポインタ型とそうでない型とで使い分けられるようになっています。こうすることで、print_container_values関数テンプレート側は、要素の実際の型を気にせず、print_value を呼び出せます。

【上級】これでもすべてのコンテナに対応できるわけではなく、要素が key と value に分かれている std::map などでは、std::cout << value がコンパイルエラーになります。std::map用に print_container_values関数テンプレートをオーバーロードする方法などの対応策が考えられます。

ヌルポインタの場合も明示的特殊化されたバージョンに分けたいと思うかもしれませんが、要素の値が nullptr だとしても、std::vector<int*> に格納されている以上、要素の型は int* なので、std::nullptr_t型を使ってオーバーロードしたとしても、その関数が呼ばれることはありません。

#include <array>
#include <deque>
#include <iostream>
#include <vector>


template <typename T>
void print_value(T value)
{
    std::cout << value;
}

template <typename T>
void print_value(T* value)
{
    std::cout << *value;
}

void print_value(std::nullptr_t)  // これが使われることはない
{
    std::cout << "null";
}


template <typename Container>
void print_container_values(const Container& c)
{
    auto it = std::cbegin(c);
    const auto it_end = std::cend(c);

    while (it != it_end) {
        print_value(*it);
        ++it;
        if (it != it_end) {
            std::cout << ", ";
        }
    }
    std::cout << "\n";
}


int main()
{
    std::vector<int> v {0, 1, 2, 3, 4};
    std::vector<int*> vp {&v[0], &v[1], nullptr, &v[3], &v[4]};
    print_container_values(v);
    print_container_values(vp);

    std::deque<int> d {0, 1, 2, 3, 4};
    std::deque<int*> dp {&d[0], &d[1], nullptr, &d[3], &d[4]};
    print_container_values(d);
    print_container_values(dp);

    std::array<int, 5> a {0, 1, 2, 3, 4};
    std::array<int*, 5> ap {&a[0], &a[1], nullptr, &a[3], &a[4]};
    print_container_values(a);
    print_container_values(ap);
}

問題3 (応用★★) 🔗

再帰テンプレート」で階乗を求める関数テンプレートの例を取り上げましたが、この方法では実行中に再帰的な関数呼び出しが行われるので、計算に時間がかかります。クラステンプレートを用いて、Factorial<N>::value に階乗の結果が生成されるようにすれば、計算をコンパイル時点で終わらせることができます。そのような実装を作成してください。


次のように実装できます。

#include <iostream>

template <int N>
class Factorial {
public:
    static constexpr int value {N * Factorial<N - 1>::value};
};


template <>
class Factorial<0> {
public:
    static constexpr int value {1};
};

int main()
{
    std::cout << Factorial<5>::value << "\n";
}

実行結果:

120

constexpr変数に値が作られているので、計算はコンパイル時に行われ、実行時には出力を行っているだけとなります。

まだ紹介をしていないですが、関数や関数テンプレートにも constexpr を付加することができるため、クラステンプレート化しなくても実行時の計算を避けられます。


なお、変数テンプレートを使って実現することもできます。ただし、Visual Studio 2017 では正常な結果が得られません(2019 で修正)。

#include <iostream>

template <int N>
constexpr int factorial {N * factorial<N - 1>};

template <>
constexpr int factorial<0> {1};

int main()
{
    std::cout << factorial<5> << "\n";
}

実行結果:

120

問題4 (調査★★★) 🔗

std::vector<bool> は使用を避けたほうがいいとされるものの、その実現方法は理解しておく価値があります。要素を 1ビット単位で扱えるようにするために、どのような工夫がなされているか調べてみてください。


要素を 1ビット単位で扱うことでメモリ使用量を抑えるためには、データメンバとして持っている配列を、要素の型のとおりに確保するわけにはいきません。そこでたとえば、unsigned int のような型を使って 32ビットなり 64ビットなりの単位で配列を確保し、ここからビットを切り出して使います。std::vector なので実際には要素数は可変ですが、固定だとすると、次のようなコードになります。

// 要素を管理するビット配列に用いる型
using array_elem_type = unsigned int;

// array_elem_type のビット数
static constexpr std::size_t BITS {sizeof(array_elem_type int) * 8};

// 要素を管理するビット配列(Size が実際の要素数とする)
array_elem_type m_elem[Size / BITS + 1];

BITS が 32 だとすると、m_elem の要素1個で 32ビットあるので、32個の要素を管理できることになります。m_elem の要素数は、実際の要素数である Size が 10 なら 10 / 32 + 1 で 1 になりますし、Size が 100 あるなら 100 / 32 + 1 で 4 です。

このように管理されることにより、問題になるのは v[3] = true; のような要素アクセスです。添字演算子のオーバーロードでは戻り値を要素の参照型とするのが一般的です(「演算子のオーバーロード」のページを参照)。しかし、ビット配列の要素をアクセスするためには次のようにはできません。

bool& operator[](std::size_t n);

これでは演算子関数の中身をうまく実装できたとしても、bool型で返してしまっているため、バイト単位でアクセスしていることになります。v[3] = true; は 3ビット目だけを true にしなければならないので、これでは正しくありません。

この問題を解決するために、プロキシクラス (proxy class) と呼ばれる仲介役のクラスを作ります。

class vector {
public:
    // 要素を管理するビット配列に用いる型
    using array_elem_type = unsigned int;

    // array_elem_type のビット数
    static constexpr std::size_t BITS {sizeof(array_elem_type int) * 8};

    // ...

    class reference {
    public:
        operator bool() const
        {
            return (ref_elem & (1 << bit_index)) != 0;
        }

        reference& operator=(bool x)
        {
            if (x) {
                ref_elem |= (1 << bit_index);
            }
            else {
                ref_elem &= ~(1 << bit_index);
            }
            return *this;
        }

    private:
        friend class vector;

        reference(array_elem_type& elem, std::size_t bit) :
            ref_elem {elem}, bit_index {bit}
        {}

        array_elem_type& ref_elem;
        std::size_t bit_index;
    };

    reference operator[](size_type n)
    {
        reference r(m_elem[n / BITS], n % BITS);
        return r;
    }

private:
    // 要素を管理するビット配列(Size が実際の要素数とする)
    array_elem_type m_elem[Size / BITS + 1];
};

reference がプロキシクラスとして機能します。

v[3] = true のようなアクセスにより vector の operator[] が呼び出され、reference のオブジェクトが作られます。このとき、m_elem の中のアクセスしたいビットが含まれている要素と、さらにその中のどのビットなのかを表すインデックスをペアにして、reference に渡しておきます。そして、operator[] の呼び出し元に reference を返します。

reference には operator= が実装されているので、v[3] = true ではこの operator= が呼び出されることになります。ref_elem |= (1 << bit_index); が実行され、ref_elem を通して、m_elem の中の適切なビットが書き換えられます。


参考リンク 🔗



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る