特定のタイプのみを受け入れるC ++テンプレート


158

Javaでは、選択したクラスを拡張するタイプのみを受け入れるジェネリッククラスを定義できます。例:

public class ObservableList<T extends List> {
  ...
}

これは、「extends」キーワードを使用して行われます。

C ++でこのキーワードに相当する単純なものはありますか?


かなり古い質問はすでに...私はここでも欠けていると感じます(回答からも)Javaジェネリックは実際にはC ++のテンプレートに相当しないということです。類似点はありますが、imhoはJavaソリューションをC ++に直接変換する際に注意する必要があります。それらはさまざまな種類の問題に対して作成されている可能性があることを理解するためです;)
idclev 463035818

回答:


104

Boostの静的アサート機能をis_base_ofBoost Type Traitsライブラリーと組み合わせて使用することをお勧めします。

template<typename T>
class ObservableList {
    BOOST_STATIC_ASSERT((is_base_of<List, T>::value)); //Yes, the double parentheses are needed, otherwise the comma will be seen as macro argument separator
    ...
};

他のいくつかのより単純なケースでは、グローバルテンプレートを単純に前方宣言できますが、有効なタイプに対してのみ(明示的または部分的に特化して)定義します。

template<typename T> class my_template;     // Declare, but don't define

// int is a valid type
template<> class my_template<int> {
    ...
};

// All pointer types are valid
template<typename T> class my_template<T*> {
    ...
};

// All other types are invalid, and will cause linker error messages.

[2013年6月12日の小規模な編集:宣言されているが定義されていないテンプレートを使用すると、コンパイラーではなくリンカーのエラーメッセージが表示されます。]


静的アサートもいいです。:)
macbirdie 2009年

5
@ジョン:私は専門が完全に一致することを恐れていmyBaseTypeます。Boostを閉じる前に、そのほとんどがヘッダーのみのテンプレートコードであることを知っておく必要があります。そのため、使用しないものについては、実行時にメモリや時間のコストはかかりません。また、ここで使用する特定のもの(BOOST_STATIC_ASSERT()およびis_base_of<>)は、宣言のみ(つまり、関数や変数の実際の定義なし)を使用して実装できるため、スペースも時間もかかりません。
j_random_hacker

50
C ++ 11が登場しました。これでを使用できますstatic_assert(std::is_base_of<List, T>::value, "T must extend list")
Siyuan Ren

2
ところで、二重括弧が必要な理由は、BOOST_STATIC_ASSERTがマクロであり、余分な括弧があると、プリプロセッサーがis_base_of関数引数内のコンマを2番目のマクロ引数として解釈できないためです。
jfritz42 2015

1
@Andreyua:何が欠けているのか本当にわかりません。変数を宣言するmy_template<int> x;my_template<float**> y;、コンパイラがこれらを許可my_template<char> z;していることを確認してから、変数を宣言して許可していないことを確認できます。
j_random_hacker 2017年

134

ここで他の回答が指摘しているように、これは通常、C ++では保証されていません。C ++では、「このクラスから継承」以外の制約に基づいてジェネリック型を定義する傾向があります。あなたが本当にそれをしたいと思ったなら、C ++ 11でやることはとても簡単です<type_traits>

#include <type_traits>

template<typename T>
class observable_list {
    static_assert(std::is_base_of<list, T>::value, "T must inherit from list");
    // code here..
};

これは、C ++で期待されている多くの概念に反しています。独自の特性を定義するなどのトリックを使用することをお勧めします。例えば、多分observable_listtypedefであり、コンテナのいずれかのタイプ受け入れることを望んでいるconst_iteratorbeginし、endメンバ関数を返すことをconst_iterator。これを継承するクラスに制限すると、継承listしない独自の型を持つが、listこれらのメンバー関数とtypedefを提供するユーザーはを使用できなくなりますobservable_list

この問題には2つの解決策があり、1つは何も制約せず、アヒルのタイピングに依存することです。このソリューションの大きな欠点は、大量のエラーが発生することであり、ユーザーが理解するのが難しい場合があります。別の解決策は、インターフェイスの要件を満たすために提供されるタイプを制約する特性を定義することです。このソリューションの大きな欠点は、煩わしいと見なすことができる余分な記述が含まれることです。ただし、良い面は、独自のエラーメッセージをアラに書くことができるということですstatic_assert

完全を期すために、上記の例の解決策を示します。

#include <type_traits>

template<typename...>
struct void_ {
    using type = void;
};

template<typename... Args>
using Void = typename void_<Args...>::type;

template<typename T, typename = void>
struct has_const_iterator : std::false_type {};

template<typename T>
struct has_const_iterator<T, Void<typename T::const_iterator>> : std::true_type {};

struct has_begin_end_impl {
    template<typename T, typename Begin = decltype(std::declval<const T&>().begin()),
                         typename End   = decltype(std::declval<const T&>().end())>
    static std::true_type test(int);
    template<typename...>
    static std::false_type test(...);
};

template<typename T>
struct has_begin_end : decltype(has_begin_end_impl::test<T>(0)) {};

template<typename T>
class observable_list {
    static_assert(has_const_iterator<T>::value, "Must have a const_iterator typedef");
    static_assert(has_begin_end<T>::value, "Must have begin and end member functions");
    // code here...
};

上記の例には、C ++ 11の機能を示す多くの概念があります。好奇心旺盛な人のためのいくつかの検索用語は、可変テンプレート、SFINAE、式SFINAE、およびタイプ特性です。


2
今日まで、C ++テンプレートがダックタイピングを使用していることに気づかなかった。ちょっと変!
Andy

2
C ++Cに導入した広範なポリシーの制約を考えると、なぜtemplate<class T:list>そのような問題のあるコンセプトなのかはわかりません。先端をありがとう。
bvj 2017年

60

まだ誰も言及していない簡単な解決策は、問題を単に無視することです。私が使用しようとするとintベクターやリストなどのコンテナークラスを想定している関数テンプレートでテンプレートタイプとしてと、コンパイルエラーが発生します。粗雑でシンプルですが、問題を解決します。コンパイラーは、指定されたタイプを使用しようとし、それが失敗すると、コンパイル・エラーを生成します。

これに関する唯一の問題は、表示されるエラーメッセージが読みにくくなることです。それでも、これは非常に一般的な方法です。標準ライブラリは、テンプレートタイプからの特定の動作を期待する関数またはクラステンプレートでいっぱいであり、使用されるタイプが有効であることを確認するために何もしません。

より優れたエラーメッセージが必要な場合(またはコンパイラエラーを生成しないが、意味がない場合をキャッチしたい場合)、どの程度複雑にするかによって、Boostの静的アサートまたはBoost concept_checkライブラリ。

最新のコンパイラーには、static_assert代わりに使用できるbuilt_in があります。


7
はい、私はいつもテンプレートがC ++でのダックタイピングに最も近いものだと思っていました。テンプレートに必要なすべての要素がある場合は、テンプレートで使用できます。

@ジョン:申し訳ありませんが、頭と尾を作成することはできません。どのタイプでT、どこからこのコードが呼び出されますか?コンテキストがないと、そのコードスニペットを理解する機会がありません。しかし、私が言ったことは本当です。メンバー関数toString()を持たない型を呼び出そうとするとtoString、コンパイルエラーが発生します。
jalf

@ジョン:次回は、問題がコードにあるときは、トリガーをあまり気にしないで
投票

@jalf、わかりました。+1。これは、それを最高にしようとするだけの素晴らしい答えでした。誤読してすみません。私は、型を関数テンプレート用ではなくクラスのパラメーターとして使用することについて話していると思っていました。これは前者のメンバーであると思いますが、コンパイラーがフラグを立てるように呼び出す必要があります。
John

13

我々は使用することができますstd::is_base_ofstd::enable_if
static_assert削除することができ、上記のクラスは、カスタム実装することができたりから使用ブースト我々は参照できない場合type_traits

#include <type_traits>
#include <list>

class Base {};
class Derived: public Base {};

#if 0   // wrapper
template <class T> class MyClass /* where T:Base */ {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
    typename std::enable_if<std::is_base_of<Base, T>::value, T>::type inner;
};
#elif 0 // base class
template <class T> class MyClass: /* where T:Base */
    protected std::enable_if<std::is_base_of<Base, T>::value, T>::type {
private:
    static_assert(std::is_base_of<Base, T>::value, "T is not derived from Base");
};
#elif 1 // list-of
template <class T> class MyClass /* where T:list<Base> */ {
    static_assert(std::is_base_of<Base, typename T::value_type>::value , "T::value_type is not derived from Base");
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type base; 
    typedef typename std::enable_if<std::is_base_of<Base, typename T::value_type>::value, T>::type::value_type value_type;

};
#endif

int main() {
#if 0   // wrapper or base-class
    MyClass<Derived> derived;
    MyClass<Base> base;
//  error:
    MyClass<int> wrong;
#elif 1 // list-of
    MyClass<std::list<Derived>> derived;
    MyClass<std::list<Base>> base;
//  error:
    MyClass<std::list<int>> wrong;
#endif
//  all of the static_asserts if not commented out
//  or "error: no type named ‘type’ in ‘struct std::enable_if<false, ...>’ pointing to:
//  1. inner
//  2. MyClass
//  3. base + value_type
}

13

私の知る限り、これは現在C ++では不可能です。ただし、目的の機能を提供する新しいC ++ 0x標準に「概念」と呼ばれる機能を追加する計画があります。C ++の概念に関するこのWikipediaの記事では、より詳しく説明しています。

これで当面の問題が解決しないことはわかっていますが、新しい標準から機能を追加し始めているC ++コンパイラがいくつかあるため、概念機能をすでに実装しているコンパイラを見つけることができる場合があります。


4
コンセプトは残念ながら標準から外されています。
macbirdie 2009

4
C ++ 20には、制約と概念を採用する必要があります。
Petr Javorik

static_assert他の回答が示すように、コンセプトやSFINAE を使用しなくても可能です。JavaやC#、またはHaskell(...)から来る誰かの残りの問題は、C ++ 20コンパイラが、JavaとC#が行うように、必要な概念に対して定義チェックを行わないことです。
user7610

10

以前の答えはすべて、木にとって森を見失ってしまったと思います。

Javaジェネリックはテンプレートと同じではありません静的な手法であるコンパイル時のポリモーフィズムではなく、動的な手法である型の消去を使用します。これら2つの非常に異なる戦術がうまく機能しない理由は明らかです。

むしろ、実行時のいずれかをシミュレートするために、コンパイル時の構文を使用しようとするよりも、何で見てみましょうextends実際に行われます。スタックオーバーフローによる、ウィキペディア、拡張をサブクラス化を示すために使用されます。

C ++はサブクラス化もサポートしています。

ジェネリックの形で型消去を使用し、型チェックを実行するために拡張するコンテナークラスも表示します。C ++では、型消去機構を自分で行う必要があります。これは簡単です。スーパークラスへのポインターを作成します。

クラス全体を作成するよりも使いやすくするために、typedefにラップしてみましょう。

typedef std::list<superclass*> subclasses_of_superclass_only_list;

例えば:

class Shape { };
class Triangle : public Shape { };

typedef std::list<Shape*> only_shapes_list;
only_shapes_list shapes;

shapes.push_back(new Triangle()); // Works, triangle is kind of shape
shapes.push_back(new int(30)); // Error, int's are not shapes

さて、Listは一種のコレクションを表すインターフェースのようです。C ++のインターフェースは、単なる抽象クラス、つまり純粋な仮想メソッドのみを実装するクラスです。この方法を使用すると、概念やテンプレートの特殊化なしで、Javaの例をC ++で簡単に実装できます。また、仮想テーブルのルックアップにより、Javaスタイルのジェネリックと同じようにパフォーマンスが低下しますが、多くの場合、これは許容できる損失です。


3
「当たり前」や「誰もが知っている」などの表現を使って、当たり前のことや普遍的に知られていることを説明するのは好きではありません。明白なのは、文脈、経験、経験の文脈に関連しています。このような発言は本質的に失礼です。
3Dave 2016

2
@DavidLivelyエチケットについてこの回答を批判するのは約2年遅すぎますが、この特定の例では私もあなたに同意しません。2つの手法がうまくいかない理由を、後でではなく明白であると説明するに説明しました。私はコンテキストを提供し、そのコンテキストからの結論は明白であると述べました。それはあなたの型に正確に適合しません。
アリス

この回答の著者は、いくつかの重労働を行った後、何かが明白であると述べました。筆者が解決策が明白であると言うつもりであったとは思わない。
ルークGehorsam

10

タイプListから派生したタイプTのみを受け入れる同等のものは次のようになります。

template<typename T, 
         typename std::enable_if<std::is_base_of<List, T>::value>::type* = nullptr>
class ObservableList
{
    // ...
};

8

要旨:それをしないでください。

j_random_hackerの答えは、これを行う方法示しています。ただし、これを行うべきではないことも指摘しておきます。テンプレートの全体の要点は、互換性のある型をすべて受け入れることができることであり、Javaスタイルの型制約はそれを破ります。

Javaの型制約は機能ではなくバグです。Javaはジェネリックスの型消去を行うため、Javaは型パラメーターの値のみに基づいてメソッドを呼び出す方法を理解できないため、それらが存在します。

一方、C ++にはそのような制限はありません。テンプレートパラメータのタイプは、使用する操作と互換性のある任意のタイプにすることができます。共通の基本クラスがある必要はありません。これはPythonの「Duck Typing」に似ていますが、コンパイル時に行われます。

テンプレートの力を示す簡単な例:

// Sum a vector of some type.
// Example:
// int total = sum({1,2,3,4,5});
template <typename T>
T sum(const vector<T>& vec) {
    T total = T();
    for (const T& x : vec) {
        total += x;
    }
    return total;
}

この合計関数は、正しい演算をサポートする任意のタイプのベクトルを合計できます。これは、int / long / float / doubleなどのプリミティブと、+ =演算子をオーバーロードするユーザー定義の数値型の両方で機能します。+ =をサポートしているため、この関数を使用して文字列を結合することもできます。

プリミティブのボックス化/ボックス化解除は必要ありません。

T()を使用してTの新しいインスタンスも構築することに注意してください。これは、暗黙のインターフェースを使用するC ++では簡単ですが、型制約のあるJavaでは実際には不可能です。

C ++テンプレートには明示的なタイプ制約はありませんが、タイプセーフであり、正しい操作をサポートしないコードでコンパイルされません。


2
テンプレートを特殊化しないことを提案している場合は、それがその言語である理由も説明できますか?

1
要点はわかりますが、テンプレート引数を特定の型から派生させる必要がある場合は、通常のコンパイラエラーの嘔吐よりも、static_assertからのメッセージを解釈しやすい方がよいでしょう。
jhoffman0x 2014

1
はい、ここではC ++の方が表現力がありますが、それは一般的には良いことです(少ない方がより多く表現できるため)が、システムを完全に理解していることを確認するために、意図的に自分自身に与える力を制限したい場合があります。
j_random_hacker 14

@Curg型の特殊化は、特定の型でしか実行できないことを利用できるようにする場合に便利です。たとえば、1つのバイトが8ビット/ブールを〜通常〜保持できるとしても、ブールはそれぞれ〜通常〜1バイトです。テンプレートコレクションクラスは、ブール値に特化できるため(およびstd :: mapの場合は)、データをより密にパックしてメモリを節約できます。
thecoshman 2015

また、明確にするために、この回答は「テンプレートを特殊化しない」ではなく、テンプレートで使用できるタイプを制限するためにその機能を使用しないことを意味しています。
thecoshman 2015

6

これはプレーンなC ++では不可能ですが、BoostのBCCLを使用するなど、コンセプトチェックを使用してコンパイル時にテンプレートパラメーターを検証できます。

C ++ 20以降、概念は言語の公式機能になりつつあります。


2
まあ、それ可能ですが、コンセプトチェックはまだ良い考えです。:)
j_random_hacker 2009年

実際には、「プレーンな」C ++では不可能だったことを意味しました。;)
macbirdie 2009年

5
class Base
{
    struct FooSecurity{};
};

template<class Type>
class Foo
{
    typename Type::FooSecurity If_You_Are_Reading_This_You_Tried_To_Create_An_Instance_Of_Foo_For_An_Invalid_Type;
};

派生クラスがFooSecurity構造を継承していることを確認してください。コンパイラはすべての適切な場所で混乱します。


@Zehelvion Type::FooSecurityはテンプレートクラスで使用されます。テンプレート引数で渡されたクラスが持っていない場合FooSecurity、それを使用しようとするとエラーが発生します。テンプレート引数で渡されたクラスがFooSecurityでない場合、それはから派生しBaseたものではないことが確実です。
GingerPlusPlus 2014

2

C ++ 20コンセプトの使用法

https://en.cppreference.com/w/cpp/language/constraints cppreferenceは、明示的な概念の例として継承の使用例を示しています。

template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T);  // T is constrained by Derived<T, Base>

複数のベースの場合、構文は次のようになると思います:

template <class T, class U, class V>
concept Derived = std::is_base_of<U, T>::value || std::is_base_of<V, T>::value;
 
template<Derived<Base1, Base2> T>
void f(T);

GCC 10はそれを実装しているようです:https : //gcc.gnu.org/gcc-10/changes.htmlそして、それをUbuntu 20.04のPPAとして取得できます。https://godbolt.org/私のローカルGCC 10.1はまだ認識してconceptいなかったので、何が起こっているのかわかりません。


1

C ++でこのキーワードに相当する単純なものはありますか?

番号。

達成しようとしていることに応じて、適切な(またはさらに優れた)代替が存在する場合があります。

私はいくつかのSTLコードを調べました(Linuxでは、それはSGIの実装から派生したコードだと思います)。「コンセプトアサーション」があります。たとえば、*xおよびを理解する型が必要な場合、++xアサーションの概念には、そのコードが何もしない関数(または同様の何か)に含まれます。ある程度のオーバーヘッドが必要になるため、定義がに依存するマクロに配置する方が賢明な場合があります#ifdef debug

サブクラスの関係が本当に知りたい場合は、コンストラクターでそれをアサートできますT instanceof list(ただし、C ++ではスペルが異なる)。このようにして、コンパイラーからチェックすることができないようにして、コンパイラーから出る方法をテストできます。


1

そのような型チェックにはキーワードはありませんが、少なくとも規則正しい方法で失敗するコードをいくつか置くことができます。

(1)関数テンプレートで特定の基本クラスXのパラメーターのみを受け入れる場合は、それを関数のX参照に割り当てます。(2)関数を受け入れるがプリミティブを受け入れない、またはその逆の場合、または他の方法でクラスをフィルター処理する場合は、受け入れたいクラスに対してのみ定義されている関数内の(空の)テンプレートヘルパー関数を呼び出します。

(1)と(2)をクラスのメンバー関数でも使用して、クラス全体でこれらの型チェックを強制できます。

あなたはおそらくあなたの痛みを和らげるためにそれをいくつかのスマートなマクロに入れることができます。:)


-2

まあ、あなたはこのようなものを読んであなたのテンプレートを作成することができます:

template<typename T>
class ObservableList {
  std::list<T> contained_data;
};

ただし、これにより制限が暗黙的になります。さらに、リストのように見えるものだけを提供することはできません。使用されるコンテナータイプを制限する方法は他にもあります。たとえば、すべてのコンテナーに存在しない特定のイテレータータイプを利用する方法がありますが、これも明示的な制限よりも暗黙的です。

私の知る限りでは、Javaステートメントを完全に反映する構文は、現在の標準には存在しません。

テンプレート内で特定のtypedefを使用して、作成したテンプレート内で使用できるタイプを制限する方法があります。これにより、特定のtypedefを含まない型のテンプレート特殊化のコンパイルが失敗することが保証されるため、特定の型を選択的にサポートするか、またはサポートしないことができます。

C ++ 11では、概念を導入することでこれが容易になるはずですが、私が望んでいるとおりに機能するとは思いません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.