LoginSignup
33
15

More than 3 years have passed since last update.

C++ のテンプレート型引数に制約を付ける

Last updated at Posted at 2019-04-25

C++ ではテンプレートはとても便利な機能であり、これ無しでのプログラミングはもはや考えられないほど馴染んだ存在です。 入力されるオブジェクトの型にかかわらず適用できるので幅広く使える関数、またはクラスを定義できます。

しかし、問題もあります。 つい最近も、そのことに言及する文言を見る機会がありました。

なんでも受け取ってしまうので渡すとまずいものを渡したときにコンパイルエラーが爆発します。まあgcc/clangの場合はそれでも慣れれば読めますが・・・。
はじめてのC++【41日目】: yumetodo 氏によるコメント

C++ のプログラムに間違いがあった場合に処理系はしばしば巨大なエラーメッセージを出力しますが、テンプレートはその要因のひとつになることがあります。

巨大なエラーを出す

では、元記事で取り上げているテンプレートの例をあらためて見てみましょう。

1.h
#ifndef TEMPLATE_H
#define TEMPLATE_H
#include <iostream>
template <typename TYPE_X, typename TYPE_Y, typename TYPE_Z> //3つ定義する
void xyz(TYPE_X x, TYPE_Y y, TYPE_Z z ){
 std::cout << x + z <<std::endl;
 std::cout << x  <<std::endl;
 std::cout << y  <<std::endl;
 std::cout << z  <<std::endl;
}

#endif //#ifndef TEMPLATE_H
2.cpp
#include "1.h"
#include <iostream>

int main(){
 double a = 1.5;
 std::string b = "Hello World";
 int c = 3;

 xyz(a,b,c);
}

はじめてのC++【41日目】 : templateの仕組み

ここで定義されているテンプレート自体は C++ の正しいコードになっていますが、間違った使い方をした場合にどうなるのか実験してみます。

3.cpp
#include "1.h"
#include <iostream>

int main(){
  double a = 1.5;
  std::string b = "Hello World";
  struct foo{};

  xyz(a, b, foo());
}

エラーになるのは上記のテンプレート xyz には以下ような暗黙の想定があるからです。

  • TYPE_XTYPE_Z とは足し算可能であり、その結果は std::cout で表示可能である
  • TYPE_X, TYPE_Y, TYPE_Z はいずれも std::cout で表示可能である

ここで定義した型 foo はこの想定を満たしませんので、もちろんエラーになります。 GCC (g++ 7.4.0) でコンパイルしてみたところ、そのエラーメッセージは 249 行になりました。

制約

テンプレート引数の型に C++11 の範囲内で制約を付けてみます。

xyz.h
#ifndef HEADER_05f933b9fdddd443c50924c37176003f
#define HEADER_05f933b9fdddd443c50924c37176003f

#include <type_traits>

template<class T, class U>
class is_addable {
private:
  template<class, class, class = void>
  struct helper : public std::false_type {};

  template<class V, class W>
  class helper<V, W, typename std::enable_if<bool(sizeof(V()+W()))>::type>
    : public std::true_type {};

public:
  static constexpr bool value = helper<T, U>::value;
};

#include <iostream>

template<class T>
class is_showable {
private:
  template<class, class = void>
  struct helper : public std::false_type {};

  template<class V>
  class helper<V, typename std::enable_if<bool(sizeof(std::cout<<V()<<std::endl))>::type>
    : public std::true_type {};

public:
  static constexpr bool value = helper<T>::value;
};

template <class X, class Y, class Z>
class is_xyz_requires {
public:
  static constexpr bool value
  = is_addable<X,Z>::value
    && is_showable<decltype(X()+Z())>::value
    && is_showable<X>::value
    && is_showable<Y>::value
    && is_showable<Z>::value;
};

template <typename X, typename Y, typename Z>
typename std::enable_if<is_xyz_requires<X, Y, Z>::value>::type
xyz(X x, Y y, Z z) {
  std::cout << x + z <<std::endl;
  std::cout << x  <<std::endl;
  std::cout << y  <<std::endl;
  std::cout << z  <<std::endl;
}

#endif

今のところ C++ のテンプレートで型に制約を付けようとすると、うんざりするような SFINAE の規則を活用した回りくどくて長々としたものになってしまいますが、効果的ではあります。 試してみましょう。

test_xyz.cpp
#include <string>
#include "xyz.h"

int main() {
  double a = 1.5;
  std::string b = "Hello World";
  int c = 3;
  struct foo {};

  xyz(a, b, c);
  xyz(a, b, foo()); // 間違った使い方
}

GCC のエラーメッセージはこうなります。

In file included from test_xyz.cpp:2:0:
xyz.h: In instantiation of 'constexpr const bool is_xyz_requires<double, std::__cxx11::basic_string<char>, main()::foo>::value':
xyz.h:49:1:   required by substitution of 'template<class X, class Y, class Z> typename std::enable_if<is_xyz_requires<X, Y, Z>::value>::type xyz(X, Y, Z) [with X = double; Y = std::__cxx11::basic_string<char>; Z = main()::foo]'
test_xyz.cpp:11:18:   required from here
xyz.h:41:32: error: no match for 'operator+' (operand types are 'double' and 'main()::foo')
     && is_showable<decltype(X()+Z())>::value
                             ~~~^~~~
test_xyz.cpp: In function 'int main()':
test_xyz.cpp:11:18: error: no matching function for call to 'xyz(double&, std::__cxx11::string&, main()::foo)'
   xyz(a, b, foo()); // 間違った使い方
                  ^
In file included from test_xyz.cpp:2:0:
xyz.h:49:1: note: candidate: template<class X, class Y, class Z> typename std::enable_if<is_xyz_requires<X, Y, Z>::value>::type xyz(X, Y, Z)
 xyz(X x, Y y, Z z) {
 ^~~
xyz.h:49:1: note:   substitution of deduced template arguments resulted in errors seen above

エラーメッセージは 16 行です。 改良前の 249 行もあったエラーメッセージに比べるとかなり短くなっていることがわかります。

何が違うのか

改良前のエラーは「テンプレートに当てはめた結果としてここが駄目だった」なのに対して、改良後のエラーは「(必要な性質を満たさないので) テンプレートに当てはめることが出来なかった」というエラーなのです。

巨大なエラーメッセージの理由は、間違った型のままでテンプレートに当てはめると型が合わない箇所が連鎖的に発見されてしまうからです。 当てはめる前に当てはめられないことがわかればそのようなエラーメッセージの出力を抑制できます。

所感

念のために申し添えておきますが、上で示したコードからもわかるようにテンプレートにきちんとした制約をつけようとすると (現状の C++ の機能の範囲内では) それはそれで難解なものになってしまいますのでどのくらい厳格に制約を付けるかというのは一概に言えるものではありません。

ただ、広く使うつもりがあるライブラリにするならある程度はきちんと制約を付けた方が良いでしょう。 人類は駄目なので各テンプレートに渡す型にどんな性質が必要なのかきちんと把握しておくことは難しいです。 エラーメッセージを見ながら修正する場面もあります。 そのときに本質的ではないエラーメッセージに埋もれた肝心な通知を探すような作業はしたくないものです。

プログラミングをしていると必要な機能を書くことに意識を向けがちです。 しかし以前にも述べたように、間違った使い方への対応、動いて欲しくないときのことにもいくらか意識を向けるべきです。 特に汎用性の高いライブラリは予想もつかない形で使われますし、規模が大きくなれば (あるいは規模が小さくても!) 作者自身でも間違えます。

制約の付け方にはある程度のイディオムは確立していますし、それをライブラリ化した Boost concept check library などを利用すればそれほど冗長にならずに済む場合もあります。 更に将来の C++ (C++20) では待望のコンセプトがいよいよ導入されるので、制約の記述はずっと楽になるはずです。

33
15
2

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
33
15