특정 유형 만 허용하는 C ++ 템플릿


158

Java에서는 다음과 같이 선택한 클래스를 확장하는 유형 만 허용하는 일반 클래스를 정의 할 수 있습니다.

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

이것은 "extends"키워드를 사용하여 수행됩니다.

C ++에서이 키워드에 해당하는 간단한 것이 있습니까?


꽤 오래된 질문이 이미 있습니다 ... 여기서 누락 된 것이 있다고 생각합니다 (답변에서) Java 제네릭은 실제로 C ++의 템플릿과 동일하지 않습니다. 비슷한 점이 있지만 imho는 자바 솔루션을 C ++로 직접 변환하여 다른 종류의 문제에 대해 만들어 질 수 있다는 사실을 인식하기 위해주의해야합니다.)
idclev 463035818

답변:


104

Boost Type Traits 라이브러리 와 함께 Boost의 정적 어설트 기능을 사용하는 것이 좋습니다 is_base_of.

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.

[Minor EDIT 6/12/2013 : 선언되었지만 정의되지 않은 템플릿을 사용하면 컴파일러가 아니라 링커 가 발생하고 오류 메시지가 나타납니다.]


정적 어설 션도 좋습니다. :)
macbirdie 2016 년

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
BTW에서 이중 괄호가 필요한 이유는 BOOST_STATIC_ASSERT가 매크로이고 추가 괄호가 프리 프로세서가 is_base_of 함수 인수 내의 쉼표를 두 번째 매크로 인수로 해석하지 못하기 때문입니다.
jfritz42

1
@Andreyua : 나는 무엇이 빠졌는지 정말로 이해하지 못합니다. 변수 선언을 시도 my_template<int> x;하거나 my_template<float**> y;컴파일러가이를 허용하는지 확인한 다음 변수를 선언하고 허용 my_template<char> z;하지 않는지 확인할 수 있습니다.
j_random_hacker

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_iterator하는 beginend멤버 함수 를 허용하려고 할 수 const_iterator있습니다. 상속하는 클래스로 이것을 제한하면 상속 list하지 않지만 list이러한 멤버 함수 및 typedef를 제공 하는 자체 유형을 가진 사용자는 을 사용할 수 없습니다 observable_list.

이 문제에 대한 두 가지 해결책이 있습니다. 그중 하나는 아무것도 구속하지 않고 오리 타이핑에 의존하는 것입니다. 이 솔루션의 큰 단점은 사용자가 이해하기 어려운 막대한 양의 오류를 포함한다는 것입니다. 또 다른 솔루션은 인터페이스 요구 사항을 충족시키기 위해 제공된 유형을 제한하는 특성을 정의하는 것입니다. 이 솔루션의 가장 큰 단점은 추가 글쓰기를 포함하여 성가신 것으로 보일 수 있다는 것입니다. 그러나 긍정적 인 측면은 자신의 오류 메시지를 la로 작성할 수 있다는 것입니다 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

60

아직 언급하지 않은 간단한 해결책은 문제를 무시하는 것입니다. int벡터 또는 목록과 같은 컨테이너 클래스를 기대하는 함수 템플릿에서 템플릿 유형으로을 사용하려고 하면 컴파일 오류가 발생합니다. 조잡하고 단순하지만 문제를 해결합니다. 컴파일러는 지정한 유형을 사용하려고 시도하고 실패하면 컴파일 오류를 생성합니다.

그것의 유일한 문제는 당신이 얻는 오류 메시지를 읽기 까다로워 질 것입니다. 그럼에도 불구하고이 작업을 수행하는 매우 일반적인 방법입니다. 표준 라이브러리는 템플릿 유형에서 특정 동작을 예상하는 함수 또는 클래스 템플릿으로 가득 차 있으며 사용 된 유형이 유효한지 확인하지 않습니다.

더 멋진 오류 메시지를 원하거나 (컴파일러 오류를 생성하지 않지만 여전히 이해가되지 않는 사례를 포착하려는 경우) 복잡한 방법에 따라 Boost의 정적 어설 션 또는 Boost concept_check 라이브러리.

최신 컴파일러를 사용하면 built_in static_assert이 있으며 대신 사용할 수 있습니다.


7
예, 항상 템플릿이 C ++로 타이핑하는 데 가장 가까운 것으로 생각했습니다. 템플릿에 필요한 모든 요소가있는 경우 템플릿에서 사용할 수 있습니다.

@ 존 : 미안 해요, 나는 그것의 머리 나 꼬리를 만들 수 없습니다. 어떤 유형이며이 T코드는 어디에서 호출됩니까? 컨텍스트가 없으면 해당 코드 스 니펫을 이해할 가능성이 없습니다. 그러나 내가 말한 것은 사실입니다. 멤버 함수 toString()가없는 유형 을 호출하려고 toString하면 컴파일 오류가 발생합니다.
jalf

@John : 다음 번에는 아마도 코드에 문제가있을 때 약간 덜 행복하게 행동하는 사람들이 될 것입니다.
jalf

@jalf, 알았어. +1. 이것은 최선을 다하려는 훌륭한 답변이었습니다. 잘못 읽어서 죄송합니다. 함수 템플릿이 아닌 클래스의 매개 변수로 유형을 사용하는 것에 대해 이야기하고 있다고 생각했습니다. 이는 전자의 멤버이지만 컴파일러가 플래그를 지정해야한다고 가정합니다.
John

13

우리는 std::is_base_ofand 를 사용할 수 있습니다 std::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 ++ Concepts에 관한 이 Wikipedia 기사 에서 자세히 설명합니다.

나는 이것이 즉각적인 문제를 해결하지는 못한다는 것을 알고 있지만 이미 새로운 표준에서 기능을 추가하기 시작한 일부 C ++ 컴파일러가 있으므로 이미 개념 기능을 구현 한 컴파일러를 찾을 수 있습니다.


4
불행히도 표준에서 개념이 삭제되었습니다.
macbirdie

4
C ++ 20에는 제약과 개념이 채택되어야합니다.
Petr Javorik 2016 년

static_assert다른 답변에서 볼 수 있듯이 SFINAE를 사용하여 개념이 없어도 가능합니다 . Java 또는 C # 또는 Haskell (...)에서 오는 누군가의 나머지 문제는 C ++ 20 컴파일러가 Java 및 C #이 수행하는 필수 개념에 대해 정의 검사 를 수행하지 않는다는 것입니다.
user7610

10

나는 모든 사전 답변이 나무의 숲을 보지 못했다고 생각합니다.

Java 제네릭 은 템플릿과 동일하지 않습니다 . 이들은 사용 형 소거 A는, 동적 기술 보다는 컴파일 시간 다형성 이며, 정적 기법 . 이 두 가지 매우 다른 전술이 잘 젤리 지 않는 이유는 분명해야합니다.

컴파일 타임 구문을 사용하여 런타임을 시뮬레이션하는 대신 extends실제로 수행 하는 작업을 살펴 ​​보겠습니다 . Stack OverflowWikipedia에 따르면 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 ++의 인터페이스는 단지 추상 클래스, 즉 순수한 가상 메소드 만 구현하는 클래스 일뿐입니다. 이 방법을 사용하면 개념이나 템플릿 전문화없이 C ++로 Java 예제를 쉽게 구현할 수 있습니다. 가상 테이블 조회로 인해 Java 스타일 제네릭만큼 느리게 수행되지만 종종 허용되는 손실이 될 수 있습니다.


3
나는 "명백해야한다"또는 "모두 알고있다"와 같은 문구를 사용하는 대답의 팬이 아니며, 명백하거나 보편적으로 알려진 것을 계속 설명합니다. 분명한 것은 상황, 경험 및 경험의 맥락과 관련이 있습니다. 그러한 진술은 본질적으로 무례합니다.
3Dave

2
@DavidLively 에티켓에 대한이 답변을 비판하기에는 약 2 년이 걸렸습니다. 나는 두 기술이 왜 그렇게 명확 하지 않다고 진술 하기 전에 함께 가지 않는지 설명했다 . 나는 맥락을 제시 한 다음 그 맥락에서 결론은 분명하다고 말했다. 정확히 금형에 맞지 않습니다.
Alice

이 답변의 저자는 약간의 리프팅을 한 후에 분명한 것이 있다고 말했습니다. 저자는 해결책이 분명하다고 말하지 않았다.
Luke 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는 제네릭에서 유형 삭제를 수행하므로 유형 매개 변수 값만으로 메소드를 호출하는 방법을 알 수 없기 때문에 존재합니다.

반면에 C ++에는 그러한 제한이 없습니다. 템플릿 매개 변수 유형은 사용되는 작업과 호환되는 모든 유형이 될 수 있습니다. 공통 기본 클래스가 필요하지 않습니다. 이것은 파이썬의 "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

1
그렇습니다. C ++는 여기에서 좀 더 표현력이 있지만, 일반적으로 좋은 표현이지만 (더 적게 표현할 수 있기 때문에) 우리는 시스템을 완전히 이해한다는 확신을 얻기 위해 의도적으로 우리 자신에게주는 힘을 제한 하려고 합니다.
j_random_hacker

@Curg 유형 전문화는 특정 유형에 대해서만 수행 할 수있는 기능을 활용할 수있을 때 유용합니다. 예를 들어, 1 바이트는 8 비트 / 부울을 보유 할 수 있지만 boolean은 ~ 정상적으로 1 바이트입니다. 템플릿 컬렉션 클래스는 (그리고 std :: map의 경우) 부울을 전문화 할 수 있으므로 메모리를 절약하기 위해 데이터를 더 단단히 패킹 할 수 있습니다.
thecoshman

또한 명확히하기 위해이 대답은 "템플릿을 전문화하지 마십시오"라고 말하는 것이 아니라 템플릿과 함께 사용할 수있는 유형을 제한하기 위해 해당 기능을 사용하지 않는다는 것입니다.
thecoshman

6

일반 C ++에서는 불가능하지만 개념 검사 (예 : Boost의 BCCL 사용)를 통해 컴파일 타임에 템플릿 매개 변수를 확인할 수 있습니다 .

C ++ 20부터 개념은 언어 의 공식 기능 이되었습니다 .


2
음, 그것은 이다 가능하지만, 개념 검사는 여전히 좋은 생각이다. :)
j_random_hacker

실제로 "일반"C ++에서는 불가능하다는 것을 의미했습니다. ;)
macbirdie 2016 년

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

2

C ++ 20 개념 사용법

https://ko.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 코드를 살펴 보았습니다 (리눅스에서는 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.