9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

std::vector<T>::iteratorが引数の関数をつくる

Last updated at Posted at 2021-03-02

はじめに

テンプレート引数Tを具体化せずに、std::vector<T>::iteratorを引数に持つ関数をつくりたい。
具体的にはこんな関数を実装したい。

template<typename T>
void f(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end)
{
    //...
}

しかし、これはコンパイルエラーになる。
色々と検索したら一般化した話は出てくるんだけど、C++難しい…ってなる話ばかりなので、上の目的に特化して解決策を書いてみる。

背景

外部ライブラリに、void f(float* p, int n)のように配列を生ポインタと長さを表す整数で受ける関数があり、iterator入れられたら便利だなぁってことでラッパー関数を実装しようとした時のこと。
STLのコンテナクラスには、ポインタで要素に連続アクセスできるもの(例: std::vector<T>)とできないもの(例: std::deque<T>)があるので、前者はそのままポインタでぶちこんで、後者はvector等にコピーした後ぶちこむ、というように処理を分けたかった。
前者はstd::array<T, N>std::vector<T>ぐらいなので、とりあえず使用頻度の高いstd::vector<T>のみを考える。
めんどくさがりなので、std::vector<T>のテンプレート引数Tを抽象化したまま扱いたかったんだけど、上記のような素直な書き方はコンパイルできませんでしたってお話。

実装

コンパイルに失敗する原因

そもそもコンパイルに失敗する原因を明確にしておこう。
Wandboxでclang13.0.0を指定して下のコードを実行してエラーを確認する。

#include <iostream>
#include <algorithm>
#include <vector>

template<typename T>
void f(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end)
{
    // ただvectorの中身を表示するだけ
    std::for_each(begin, end, [](const auto& x) { std::cout << x << " "; });
    std::cout << std::endl;
}

int main()
{
    std::vector<float> v = {1.f, 2.f, 3.f};
    f(v.begin(), v.end());
    
    return 0;
}
prog.cc:16:5: error: no matching function for call to 'f'
    f(v.begin(), v.end());
    ^
prog.cc:6:6: note: candidate template ignored: couldn't infer template argument 'T'
void f(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end)
     ^
1 error generated.

テンプレート引数Tを推測できないらしい。
調べると::の左側にあるテンプレート引数は推測できないことがわかった1。言語仕様なので、仕方がない。

テンプレート引数Tのところにfloat等の具体的な型を入れてオーバーロードする手もあるが、同じ処理するならなるべくまとめたい。

解決策1

std::vector<T>のテンプレート引数Tが推測できないなら、推測できる形に変形すればいい。
具体的には、以下のようにiterator自体をテンプレート引数とした関数で受け取り、そこからstd::vector<T>のテンプレート引数Tを推測してから、元の関数に入れてやる。

#include <iostream>
#include <algorithm>
#include <vector>

template<typename T>
void f(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end)
{
    // ただvectorの中身を表示するだけ
    std::cout << "vector specialized" << std::endl;
    std::for_each(begin, end, [](const auto& x) { std::cout << x << " "; });
    std::cout << std::endl;
}

template<typename Iterator, typename T = typename Iterator::value_type>
void g(Iterator begin, Iterator end)
{
    f<T>(begin, end);
}

int main()
{
    std::vector<float> v = {1.f, 2.f, 3.f};
    g(v.begin(), v.end());

    return 0;
}

これは無事にコンパイルできるし、正常に動作する。
これで、テンプレート引数Tを具体化せずに、std::vector::iteratorを引数に持つ関数をつくることができました。
めでたしめでたし。

…と言いたいところだが、当初の目的はSTLのコンテナクラスのうち、ポインタで要素に連続アクセスできるもの(例: std::vector<T>)とできないもの(例: std::deque<T>)のそれぞれに対して、iteratorを受け取る関数を別々に定義したい、というところだった。

上記の解決策1では、value_typeを持つ型であれば全て関数gを通るため、テンプレート引数Iteratorstd::vector<T>::iteratorなのか、std::deque<T>::iteratorなのかは関数fで分けることになる。
std::vector<T>のiteratorに対してのみ適用される形で関数fを書いているが、実際にはstd::deque<T>std::set<T>に対してもそれぞれ関数fをオーバーロードして書かなければならず、めんどくさい。
std::vector<T>以外のiteratorを受け取る関数をテンプレートでまとめようとすると、今度は関数gの中身もめんどくさいことになる。

解決策2

結局、要素型Tを受け取りつつ、任意のiteratorをテンプレートで受け取れるような形をつくってあげる。
具体的には、先ほどの関数gをクラステンプレートの静的メンバー関数にしてあげればよい。

#include <iostream>
#include <algorithm>
#include <vector>
#include <deque>

template<typename T>
void print(const T* p, int n)
{
    // ただ中身を表示するだけ
    std::for_each(p, p + n, [](const auto& x) { std::cout << x << " "; });
    std::cout << std::endl;
}

template<typename T>
class Foo
{
public:
    // vector専用
    // 実際には std::vector<T>::const_iterator も考慮する必要がある
    static void f(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator end)
    {
        std::cout << "vector specialized" << std::endl;
        print(&(*begin), static_cast<int>(end - begin)); // vectorは生ポによる要素の連続アクセス可能
    }
    
    // 抽象化したやつ
    template<typename Iterator>
    static void f(Iterator begin, Iterator end)
    {
        std::cout << "generalized" << std::endl;
        std::vector<T> tmp(begin, end); // vectorにコピーして生ポ扱えるようにしてからぶちこむ
        print(tmp.data(), static_cast<int>(tmp.size()));
    }
};

template<typename Iterator, typename T = typename Iterator::value_type>
void g(Iterator begin, Iterator end)
{
    Foo<T>::f(begin, end);
}

int main()
{
    std::vector<float> v = {1.f, 2.f, 3.f};
    std::deque<float> d = {1.f, 2.f, 3.f};
    
    std::cout << "=====vector=====" << std::endl;
    g(v.begin(), v.end());
    
    std::cout << "=====deque=====" << std::endl;
    g(d.begin(), d.end());  
    
    return 0;
}

これを実行すると、以下のように、std::vector<T>に対しては専用のもの、std::deque<T>に対してはテンプレート引数Iteratorで抽象化したものが呼び出されていることがわかる。

=====vector=====
vector specialized
1 2 3 
=====deque=====
generalized
1 2 3

これで、std::vector<T>と、ポインタで要素に連続アクセスできないもの(例: std::deque<T>)のそれぞれに対し、iteratorを受け取る関数を定義できました。めでたしめでたし。

おわりに

こんな形で実装しましたが、もっと簡単にできるよ!って方法あったら教えてください。
C++難しい… 1ミリもわからん…

余談

今回、STLのコンテナクラスのうち、ポインタで要素に連続アクセスできるものとしてstd::vector<T>のみを扱ったが、実際には固定長配列のstd::array<T, N>もある。
これも同様にstd::array<T, N>::iteratorを引数に持つ関数を作ろうとするんだけど、配列長を表すテンプレート引数Niteratorから取得できない…
そもそもstd::array<T, N>iteratorは完全に実装依存なので、例えばmsvcでは_Array_Iterator<T, N>というクラスなんだけど、gccではT*のように生ポインタで定義されているので、後者の場合はどうやってもテンプレート引数Nは推測できない…

SFINAEでごにょごにょすれば出来るかもしれんけど、進んで沼には入りたくないよねぇ…

  1. C++関数テンプレートと半順序とオーバーロード

9
7
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?