27
14

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 5 years have passed since last update.

std::vector<const T>は使えないのか?

Posted at

std::vectorの要素をconstオブジェクトにした時の挙動がコンパイラごと変わって混乱したのでメモ.

tl;dr

std::vector<const T>はコンパイルが通り問題なく使えるか?

  • 規格ではアロケータが扱える型はcv-unqualifiedであることを要求している(N4659 §20.5.3.5
    • つまりアロケータを取るSTLコンテナはconst修飾されたオブジェクトを扱えないはず
  • g++(libstdc++)の場合
    • gcc8だと規格通りstd::vector<const T, A>を実体化するとstatic_assertでコンパイルエラー
    • gcc7だとstd::allocatorをconst Tに対して使うとコンパイルエラー,自前で無理やりconst Tに対しても使えるアロケータを作ればconstオブジェクトを扱えてしまう
  • clang(libc++)の場合
    • clang7でis_trivially_move_constructible_v<T> == falseだとconstオブジェクトを扱えてしまう
  • VC++
    • 知らん

つまり,std::vector<const T, A>は一部の実装ではコンパイルができてしまうが使うべきではない.

はじめに

vector<const T>のように要素をconstにすることはあまりないと思いますが,constにして試したところコンパイラ依存,バージョン依存でコンパイル結果が変わって混乱したので規格書と標準ライブラリの実装を参照して調べました.N4659(と一部N3337)を参照しました.本記事で出てくる§X.Y.ZはN4659でのセクション番号に対応します.筆者はC++初心者なのでC++初心者以上の方はマサカリをご準備ください.
想定するコードは以下のようにvector::reservevector::emplace_back(or push_back)を使うものです.

vector<const T> v;
v.reserve(2);
v.emplace_back(1);
v.emplace_back(42);

vector requirement

規格を全部は読んでないので漏れがあるかもしれないですが,上のコードがコンパイルできるためには,Tをvectorの要素型,AをAllocatorとして,

  • Tがvector<T, A>からErasuable(§26.2.1 Table 83
  • Tがvector<T, A>に対してMoveInsertable(厳密には*thisに対して)(§26.3.11.3 第2段落,vector::reserveの要請)

である必要があります.Erasuable,MoveInsertibleそれぞれの定義は以下の通りです.ただし,std::vector<T, A>としてaAの左辺値,pをTのポインタ,rvをTの右辺値とします.

  • Erasuable:allocator_traits<A>::destroy(a, p)がwell-formed(§26.2.1 (15.6)
  • MoveInsertible:allocator_traits<A>::construct(a, p, rv)がwell-formed(§26.2.1 (15.3)

std::allocator_traits<A>::destroy(a, p)は通常a.destroy(p)を呼び,std::allocator_traits<A>::construct(a, p, args...)は通常a.construct(p, std::forward<Args>(args)...) を呼び出します(§23.10.8.2).
つまり,文言的にはアロケータがconstオブジェクトに対応していればvectorの要素をconstにしても問題ないように見えます.

allocator requirement

上記のようにvectorでconstオブジェクトを扱えるかどうかはアロケータ次第ということがわかりました.なので次にallocatorの仕様§20.5.3.5を確認します.
§20.5.3.5Table 30を見るとT, U, C any cv-unqualified object typeと書かれており,§20.5.3.5 第2段落には以下のような記述があります.

Table 30 describes the types manipulated through allocators. Table 31 describes the requirements on allocator types and thus on types used to instantiate allocator_traits.

つまり,アロケータが扱える型はany cv-unqualified object typeとなるため,そもそも規格的にアロケータはconst volatile修飾された型を扱えないようです.
このstackoverflowの回答にあるようにアロケータが扱える型は変遷しており,C++03では型に関する規定はなく,C++11ではnon const,non referenceな型,C++14ではnon constな型,C++17ではcv修飾されていない型のような変遷をしてきているようです.C++11以降ではconstな型はどのみち扱えないです.
この時点で,const修飾された型をアロケータを取るSTLコンテナの要素にできないという結論になります.以下で各コンパイラの対応を見ます.

gccの場合

gcc8

#include <vector>
int main() {
    std::vector<const int> v;
    return 0;
}

上のコードをコンパイルすると問答無用でstatic_assertで以下のようなコンパイルエラーになります(一部のみ抜粋).

/opt/wandbox/gcc-head/include/c++/8.0.1/bits/stl_vector.h:351:21: error: static assertion failed: std::vector must have a non-const, non-volatile value_type
       static_assert(is_same<typename remove_cv<_Tp>::type, _Tp>::value,

つまり,規格通りgcc8ではどんなアロケータを使ってもcv修飾された型を要素にすることはできません.また,このようにcv修飾した型に対する標準アロケータstd::allocator<T>はinvalidとなっています

gcc7

次にgcc7での挙動を見ます.

標準アロケータを使った場合

gcc8の検証の時と同様,以下のコードをコンパイルしてみます.

#include <vector>
int main() {
    std::vector<const int> v;
    return 0;
}

すると,今度はstatic_assertは出ず,別のエラーがでます(今回も一部のみ抜粋).

/opt/wandbox/gcc-7.1.0/include/c++/7.1.0/ext/new_allocator.h:93:7: error: 'const _Tp* __gnu_cxx::new_allocator<_Tp>::address(__gnu_cxx::new_allocator<_Tp>::const_reference) const [with _Tp = const int; __gnu_cxx::new_allocator<_Tp>::const_pointer = const int*; __gnu_cxx::new_allocator<_Tp>::const_reference = const int&]' cannot be overloaded
       address(const_reference __x) const _GLIBCXX_NOEXCEPT
       ^~~~~~~
/opt/wandbox/gcc-7.1.0/include/c++/7.1.0/ext/new_allocator.h:89:7: error: with '_Tp* __gnu_cxx::new_allocator<_Tp>::address(__gnu_cxx::new_allocator<_Tp>::reference) const [with _Tp = const int; __gnu_cxx::new_allocator<_Tp>::pointer = const int*; __gnu_cxx::new_allocator<_Tp>::reference = const int&]'
       address(reference __x) const _GLIBCXX_NOEXCEPT

・・・中略・・・
/opt/wandbox/gcc-7.1.0/include/c++/7.1.0/ext/new_allocator.h:121:23: error: invalid conversion from 'const void*' to 'void*' [-fpermissive]
      ::operator delete(__p, std::align_val_t(alignof(_Tp)));
      ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

少しメッセージが長いですが,標準アロケータのaddressメンバ関数のオーバーロードとoperator deleteの引数でconst void*からvoid*への変換でコンパイルエラーが出ているようです.
std::allocator<T>::addressC++17で非推奨になったメンバ関数です.N3337の方を参照すると,標準アロケータは

template <class T> class allocator {
・・・中略・・・
    typedef T*        pointer;
    typedef const T*  const_pointer;
    typedef T&        reference;
    typedef const T&  const_reference;
・・・中略・・・
    pointer address(reference x) const noexcept;
    const_pointer address(const_reference x) const noexcept;
・・・中略・・・
};

のようになっています.つまり,T = const Uとした場合,addressメンバ関数の宣言は同一になり,コンパイルエラーになります.この挙動がcv修飾を許さないという規格を意図して作りこまれたものかわかりませんが,これがgcc7でstd::vectorの要素をconstオブジェクトにできない原因の一つです.operator deleteでのconstポインタから非constポインタへの変換エラーも意図されたものかわかりませんが同様です.

カスタムアロケータを使った場合

上記のようにgccでは規格通り標準アロケータがconstな型に対応していないです.
代わりに以下のように規格からは外れてconst対応した自前のアロケータを使った場合はvectorの要素をconstにすることができます(カスタムアロケータは適当に実装しただけです).ただし,こんなことはするべきではないでしょう.

#include <memory>
#include <vector>
#include <type_traits>
#include <iostream>

template <class T>
struct MyAllocator {
    using value_type = T;
    MyAllocator() = default;
    MyAllocator(const MyAllocator&) = default;
    MyAllocator(MyAllocator&&) = default;
    ~MyAllocator() = default;
    MyAllocator& operator=(const MyAllocator&) = default;
    MyAllocator& operator=(MyAllocator&&) = default;
    template <class U>
    MyAllocator(const MyAllocator<U>&) {}

    T* allocate(std::size_t n){ return reinterpret_cast<T*>(std::malloc(sizeof(T) * n)); }
    void deallocate(T* p, std::size_t){ std::free(const_cast<std::add_pointer_t<std::remove_cv_t<T>>>(p)); }
};
template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&){ return true; }
template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&){ return false; }

int main () {
    std::vector<const int, MyAllocator<const int>> v;
    v.reserve(1);
    v.emplace_back(42);
    std::cout << v[0] << std::endl;
    return 0;
}

clangの場合

clang7の場合,以下のようにconst std::stringを要素とするvectorを使ったコードが問題なくコンパイルでき,動作してしまいます.

#include <vector>
#include <string>
#include <iostream>
int main() {
    std::vector<const std::string> v;
    v.reserve(1);
    v.emplace_back("a");
    std::cout << v[0] << std::endl;
    return 0;
}

一方,以下のコードをコンパイルするとコンパイルエラーになります.

#include <vector>
int main() {
    std::vector<const int> v;
    v.reserve(1);
    return 0;
}

出現したコンパイルエラーは以下の通りです(一部抜粋).

/opt/wandbox/clang-head/include/c++/v1/memory:1698:31: error: cannot initialize a parameter of type 'void *' with an lvalue of type 'const int *'
                _VSTD::memcpy(__end2, __begin1, _Np * sizeof(_Tp));
                              ^~~~~~
/opt/wandbox/clang-head/include/c++/v1/vector:898:21: note: in instantiation of function template specialization 'std::__1::allocator_traits<std::__1::allocator<const int> >::__construct_backward<const int>' requested here
    __alloc_traits::__construct_backward(this->__alloc(), this->__begin_, this->__end_, __v.__begin_);
                    ^
/opt/wandbox/clang-head/include/c++/v1/vector:1542:9: note: in instantiation of member function 'std::__1::vector<const int, std::__1::allocator<const int> >::__swap_out_circular_buffer' requested here
        __swap_out_circular_buffer(__v);
        ^
prog.cc:4:7: note: in instantiation of member function 'std::__1::vector<const int, std::__1::allocator<const int> >::reserve' requested here
    v.reserve(1)

gccの時と違いmemcpyconst void*からvoid*へ変換できないというコンパイルエラーが出ています.ご存知のとおりreservepush_backemplace_backで配列サイズを変えた時に現在のcapacityが足りないと領域を再確保して再確保先に要素をムーブかコピーする必要があります.intの場合はこのコピー操作がmemcpyで最適化されているようです.memcpyによる最適化はこのように実装され,is_trivially_move_constructible_v<T>がtrueであればmemcpyによる最適化(バイトレベルでのメモリコピー)が行われます.std::vector<const T>は内部的にはcosnt Tのポインタを保持することになるため,memcpyの方が適用されると上のようにconstポインタを非constポインタに暗黙にキャストできないことによるコンパイルエラーが出現します.そのため,is_trivially_move_constructible_v<const T>がfalseになるような型(例えばstd::stringなど)であればconstにしてstd::vectorの要素にできてしまいます.
ちなみに,上で書いたようにgccの場合はstd::allocator<const T>はちゃんとinvalidでしたが,clangの場合はこのようにconst Tに対して部分特殊化されおり,gccの時に起きたaddressメンバ関数の重複が回避されています. つまり標準アロケータがconstオブジェクトを扱えるように独自拡張されています.そのため,clangでは一部の型でconst修飾したものがstd::vectorの要素にできてしまいます.

まとめ

  • 規格ではアロケータのテンプレート引数はcv-unqualifiedな型であることを要求している
    • つまりアロケータを取るSTLコンテナはconst修飾されたオブジェクトを扱えないはず
  • g++(libstdc++)の場合
    • gcc8: 規格通りvector<const T, A>を実体化するとstatic_assertでコンパイルエラー
    • gcc7: 規格に忠実にstd::allocatorを実装しているのでstd::allocatorがconstオブジェクトを扱えない.ただ自前でconst Tに対しても使えるアロケータを作れば使えてしまう
  • clang(libc++)の場合
    • clang7でis_trivially_move_constructible_v<T> == falseだとconstオブジェクトを扱えてしまう

std::vector<const T, A>はコンパイルできたとしても使わないようにしましょう.

27
14
0

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
27
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?