Reference
- Ch22, C++ Template The Complete Guide
Contents
- Function Objects, Pointers, and std::function<>
- Bridge Interface
- Type Erasure
- Optional Bridging (for lambda)
C++에는 정적 다형성(static polymorphism - via template)과 동적 다형성(dynamic polymorphism - via inheritance and virtual function)이 있다. 코드를 작성할 때 두 종류의 다형성으로 강력한 추상화가 가능하지만, 각각에는 장단점이 있다. 정적 다형성은 다형성이 적용되지 않은 일반 코드와 같은 성능을 갖지만, 런타임에 사용할 수 있는 타입 집합(set of types)이 컴파일 과정에 고정된다. 반면 상속을 사용하는 동적 다형성은 컴파일 과정에 알지 못했던 타입에 대해서도 하나의 다형적 함수(polymorphic function)으로 동작시킬 수 있다. 하지만 그 타입은 공통의 베이스 클래스로부터 상속받아야 하기 때문에 덜 유연하다.
이번 포스팅에서는 각 다형성의 장점을 얻을 수 있도록 동적 다형성과 정적 다형성을 이어주는 방법에 대해서 살펴본다. 즉, 코드 크기를 줄이고 동적 다형성의 유연성을 정적 다형성에서 적용할 수 있는 방법을 살펴보자. 이에 대한 예시로 표준 라이브러리에서 제공하는 std::function<>의 simple version을 구현한다.
Function Objects, Pointers, and std::function<>
함수 객체는 템플릿에 커스터마이즈 가능한 동작을 제공할 때 유용하다. 예를 들어, 아래의 함수 템플릿은 0부터 특정 값까지 값을 나열하는데, 각 값을 주어진 객체 f로 전달한다.
#include <iostream>
#include <vector>
void printInt(int i)
{
std::cout << i << ' ';
}
template<typename F>
void forUpTo(int n, F f)
{
for (int i = 0; i < n; ++i) {
f(i); // call passed function f for i
}
}
int main()
{
std::vector<int> values;
// insert values from 0 to 4:
forUpTo(5, [&values](int i) {
values.push_back(i);
});
// print elements:
forUpTo(5, printInt); // print 0 1 2 3 4
std::cout << std::endl;
}
forUpTo() 함수 템플릿에는 어떠한 함수 객체(lambda, function pointer, 적절한 operator()를 갖거나 함수 포인터 또는 참조로의 변환을 구현한 모든 클래스 등)라도 사용할 수 있고, forUpTo()를 사용할 때마다 서로 다른 함수 템플릿의 인스턴스가 생성된다. 위의 함수 템플릿은 크기가 작지만, 템플릿이 크다면 만들어지는 인스턴스 때문에 코드의 크기가 매우 커질 수 있다.
코드가 커지는 것을 막기 위해 템플릿이 아닌 함수 포인터를 받도록 아래와 같이 바꿀 수 있다.
void forUpTo(int n, void(*f)(int))
{
for (int i = 0; i < n; ++i) {
f(i); // call passed function f for i
}
}
위 구현에 printInt()를 전달하면 잘 동작하지만, 람다를 전달하면 에러가 발생한다.
// ERROR: lambda not convertible to afunction pointer
forUpTo1(5, [&values](int i) {
values.push_back(i);
});
// OK
forUpTo1(5, printInt);
표준 라이브러리의 클래스 템플릿 std::function<>으로도 forUpTo()를 구현할 수 있다.
void forUpTo(int n, std::function<void(int)> f)
{
for (int i = 0; i < n; ++i) {
f(i); // call passed function f for i
}
}
std::function<>은 함수 객체가 받을 파라미터 타입과 생성하는 리턴 타입을 설명하는 함수 타입이다 (함수 포인터와 유사). 이와 같은 방식으로 forUpTo()를 구현하면 정적 다형성의 일부 측면, 즉, function pointer, lambda, 적절한 operator()를 갖는 임의의 클래스 등 무한한 타입 집합에서 동작할 수 있는 능력을 제공한다. 또한, 단 하나의 구현만을 가진다는 점에서 템플릿이 아닌 함수로 남을 수 있다. 이는 타입 삭제(type erasure)이라는 기법을 사용하여 가능하며, 정적 다형성과 동적 다형성 사이를 연결해주는 역할을 한다.
Generalized Function Pointers
std::function<> 타입은 사실상 C++ function pointer의 일반화된 형태이며 다음의 기본적인 연산을 제공한다.
- 호출자가 함수 그 자체에 대해서 알고 있지 않아도 호출할 수 있다.
- 복사, 이동, 할당이 가능하다.
- (호환이 되는 signature를 가진 경우) 다른 함수로 초기화하거나 할당받을 수 있다.
- 아무 함수도 할당되어 있는 않는 "null" 상태가 존재한다.
하지만, C++의 함수 포인터와 달리 std::function<>은 람다나 적절한 operator()를 갖는 어떠한 종류의 함수 객체도 저장할 수 있는데, 이들은 타입이 모두 다를 수 있다.
아래에서 std::function<> 대신 사용할 수 있는 FunctionPtr을 구현하여, 다음의 코드가 잘 동작할 수 있도록 해보자.
#include <iostream>
#include <vector>
#include "function_ptr.h"
void printInt(int i)
{
std::cout << i << ' ';
}
void forUpTo(int n, FunctionPtr<void(int)> f)
{
for (int i = 0; i < n; ++i) {
f(i); // call passed function f for i
}
}
int main()
{
std::vector<int> values;
// insert values from 0 to 4:
forUpTo(5, [&values](int i) {
values.push_back(i);
});
// print elements:
forUpTo(5, printInt); // print 0 1 2 3 4
std::cout << std::endl;
}
FunctionPtr의 인터페이스는 상당히 직관적이며, 아래의 기능을 제공한다.
- 생성, 복사, 이동, 소멸, 초기화
- 임의의 함수 객체로부터의 할당
- 베이스 클래스의 함수 객체에 대한 호출
여기서 눈 여겨 볼만한 부분은 인터페이스 부분이 템플릿 부분 특수화 내에서 완전히 설명되는 방법인데, 부분 특수화는 템플릿 인자를 결과와 인자 타입으로 나누는 역할을 한다.
// primary template:
template<typename Signature>
class FunctionPtr;
// partial specialization:
template<typename R, typename... Args>
class FunctionPtr<R(Args...)>
{
private:
/**
* The implementation contains a single nonstatic member variable, bridge, which will be responsible for
* both storage and manipulation of the stored function object. Ownership of this pointer is tied to the
* FunctionPtr object, so most of the implementation provided merely manages this pointer.
*/
FunctorBridge<R, Args...>* bridge;
public:
// constructors:
FunctionPtr() : bridge(nullptr) {}
FunctionPtr(FunctionPtr const& other); // a definition is below
FunctionPtr(FunctionPtr& other) : FunctionPtr(static_cast<FunctionPtr const&>(other)) {}
FunctionPtr(FunctionPtr&& other) : bridge(other.bridge) {
other.bridge = nullptr;
}
// constructor from arbitrary function objects:
template<typename F>
FunctionPtr(F&& f); // a definition is below
// assignment operators:
FunctionPtr& operator=(FunctionPtr const& other) {
FunctionPtr tmp(other);
swap(*this, tmp);
return *this;
}
FunctionPtr& operator=(FunctionPtr&& other) {
delete bridge;
bridge = other.bridge;
other.bridge = nullptr;
return *this;
}
// construction and assignment from arbitrary function objects:
template<typename F>
FunctionPtr& operator=(F&& f) {
FunctionPtr tmp(std::forward<F>(f));
swap(*this, tmp);
return *this;
}
// destructor
~FunctionPtr() {
delete bridge;
}
friend void swap(FunctionPtr& fp1, FunctionPtr& fp2) {
std::swap(fp1.bridge, fp2.bridge);
}
explicit operator bool() const {
return bridge == nullptr;
}
// invocation:
R operator()(Args... args) const; // a definition is below
};
위 코드에서 아직 구현되지 않은 멤버 함수들은 아래에서 설명한다. 위 코드는 저장된 함수 객체의 저장 공간과 조작을 담당하는 bridge (FunctorBridge 타입)라는 하나의 비정적 멤버 변수를 포함한다. 이 포인터는 FunctionPtr 객체가 소유하고 있으며, 제공된 구현의 대부분은 이 포인터를 다루는 것 뿐이다.
Bridge Interface
FunctorBridge 클래스 템플릿은 함수 객체를 소유하고 조작하는 역할을 담당한다. 추상 클래스로 구현되어 FunctionPtr의 동적 다형성의 베이스가 된다.
template<typename R, typename... Args>
class FunctorBridge
{
public:
virtual ~FunctorBridge() {}
virtual FunctorBridge* clone() const = 0;
virtual R invoke(Args... args) const = 0;
};
FunctorBridge는 저장된 함수 객체를 조작하는데 필수적인 연산(destructor, clone() for copy, invoke() for call)을 가상 함수를 통해 제공한다. clone()과 invoke()는 const member function으로 정의해야 하는데, 이는 기대와 달리 const FunctionPtr 객체를 사용하여 const가 아닌 operator() 오버로딩을 호출하는 것을 막을 수 있기 때문이다.
위의 가상 함수를 사용하여 아까 구현하지 못한 FunctionPtr의 나머지 멤버 함수를 아래와 같이 구현할 수 있다.
template<typename R, typename... Args>
FunctionPtr<R(Args...)>::FunctionPtr(FunctionPtr const& other) : bridge(nullptr)
{
if (other.bridge) {
bridge = other.bridge->clone();
}
}
template<typename R, typename... Args>
R FunctionPtr<R(Args...)>::operator()(Args... args) const
{
return bridge->invoke(std::forward<Args>(args)...);
}
Type Erasure
FunctorBridge의 각 인스턴스는 추상 클래스이므로 가상 함수의 실제 구현은 파생 클래스에서 제공해야 한다. 임의의 함수 객체 전체를 지원하려면 파생 클래스의 수가 무한정 있어야 한다. 다행히 파생 클래스를 자신이 저장할 함수 객체 타입에 따라 파라미터화한다면 가능하다.
template<typename Functor, typename R, typename... Args>
class SpecificFunctorBridge : public FunctorBridge<R, Args...>
{
Functor functor;
public:
template<typename FunctorFwd>
SpecificFunctorBridge(FunctorFwd&& functor) : functor(std::forward<FunctorFwd>(functor)) {}
virtual SpecificFunctorBridge* clone() const override {
return new SpecificFunctorBridge(functor);
}
virtual R invoke(Args... args) const override {
return functor(std::forward<Args>(args)...);
}
};
SpecificFunctorBridge의 각 인스턴스는 함수 객체(타입은 Functor)의 복사본을 저장한다. 이 함수 객체는 호출될 수 있고, 복사할 수 있으며 소멸자를 통해 소멸될 수 있다. SpecificFunctorBridge 인스턴스는 FunctionPtr이 새로운 함수 객체로 초기화될 때마다 생성된다.
마지막으로 FunctionPtr에서 아직 구현하지 않은 마지막 멤버 함수 구현으로 FunctionPtr 구현을 완성한다.
template<typename R, typename... Args>
template<typename F>
FunctionPtr<R(Args...)>::FunctionPtr(F&& f) : bridge(nullptr)
{
// It uses std::decay to produce the Functor type, which makes the inferred type F suitable for storage, for
// example, by tuning references to function types info function pointer types and removing top-level const,
// volatile, and reference types.
using Functor = std::decay_t<F>;
using Bridge = SpecificFunctorBridge<Functor, R, Args...>;
bridge = new Bridge(std::forward<F>(f));
}
FunctionPtr 생성자 자체가 함수 객체 타입 F에 템플릿화되어 있는데, 이 타입은 SpecificFunctorBridge의 부분 특수화에만 알려져 있다는 것에 주목하자. 새로 할당된 Bridge 인스턴스가 bridge 멤버 변수에 할당되면 함수 객체 타입 F에 대한 추가 정보는 사라지게 된다. 이는 Bridge*에서 FunctorBridge<R, Args...>*로의 파생 클래스에서 기본 클래스로의 변환 때문이다. 이와 같이 타입 정보를 잃어버리기 때문에 정적 다형성과 동적 다형성 사이를 이어주는 이 기법을 타입 삭제(type erasure)라고 부른다.
위 구현에서 한 가지 더 주목할만 한 부분은 Functor 타입을 만들기 위해 std::decay_t를 사용했다는 점이다. 이를 사용하면 저장에 적합한 타입 F를 만들 수 있는데, 예를 들어, 함수 타입에 대한 참조는 함수 포인터 타입으로 바꾸고 top-level의 const, volatile, 그리고 참조는 제거한다.
Optional Bridgin
여기까지만 구현해도 아까 구현했던 main 함수는 잘 동작하지만, 아직 함수 포인터에서 제공하는 한 연산은 제공하지 못하고 있다. 바로 두 FunctionPtr 객체가 같은 함수를 호출하는지를 검사하는 기능이 빠져있다. 이러한 연산을 추가하려면 먼저 FunctorBridge에 equals 연산을 추가해야 한다.
virtual bool equals(FunctorBridge const* fb) const = 0;
그런 다음 SpecificFunctorBridge 내에서 오버로드 함수를 다음과 같이 구현한다.
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override {
if (auto const* specFb = dynamic_cast<SpecificFunctorBridge const*>(fb)) {
return functor == specFb->functor;
}
return false;
}
마지막으로 FunctionPtr의 operator==를 구현하여 null functors를 체크한 뒤, == 연산을 FunctorBridge로 위임한다.
friend bool operator==(FunctionPtr const& f1, FunctionPtr const& f2) {
if (!f1 || !f2) {
return !f1 && !f2;
}
return f1.bridge->equals(f2.bridge);
}
friend bool operator!=(FunctionPtr const& f1, FunctionPtr const& f2) {
return !(f1 == f2);
}
위의 구현에서 잘못된 것은 없지만, 아쉽게도 단점이 있다. 만약 적절한 operator==가 없는 함수 객체(예를 들어, 람다)가 FunctionPtr에 할당되거나 초기화되면 컴파일 에러가 발생한다. FunctionPtr의 operator==가 사용되지도 않았는데 컴파일이 되지 않으니 신기할 수 있다. 사실 다른 클래스 템플릿(std::vector 등)에서는 operator==가 사용되지 않는 한 operator==가 없는 객체에 대해서도 인스턴스화할 수 있다.
위와 같은 문제는 타입 삭제로 인해 발생한 것이다. FunctionPtr이 할당되거나 초기화되면 그 즉시 함수 객체의 타입을 상실하게 된다. 따라서 할당이나 초기화를 완료하기 전에 타입에 대한 모든 정보를 수집해야 하는 것이다. 그 정보 속에는 함수 객체의 operator==에 대한 호출 정보도 포함되어 있는데, 언제 필요할지 모르기 때문이다. 기계적으로 operator==에 대한 호출에 대한 코드는 인스턴스화된다. 클래스 템플릿의 모든 가상 함수는 클래스 템플릿 자체가 인스턴스화될 때 대부분 같이 인스턴스화되기 때문이다.
다행히 operator==를 호출하기 전에 사용 가능한지 체크하는 SFINAE 기반 trait를 사용하여 해결할 수 있다.
#include <utility>
#include <type_traits>
#include <exception>
template<typename T>
class IsEqualityComparable
{
private:
static void* conv(bool); // to check convertibility to bool
template<typename U>
static std::true_type test(decltype(conv(std::declval<U const&>() == std::declval<U const&>())),
decltype(conv(!(std::declval<U const&>() == std::declval<U const&>())))
);
// fallback
template<typename U>
static std::false_type test(...);
public:
static constexpr bool value = decltype(test<T>(nullptr, nullptr))::value;
};
이 특질은 두 개의 test() 오버로딩이 있다. 하나는 decltype을 사용하여 테스트하는 표현식을 쓰고, 다른 하나는 ...을 통해 임의의 인자를 받는다. 첫 번째 test() 함수는 T const 타입의 두 객체를 비교하려고 시도한 후, 그 결과가 모두 bool로 암묵적으로 변환될 수 있고 그 결과를 operator!()로 전달할 수 있는지 확인한다. 두 연산이 모두 잘 만들어진다면 test(void*, void*)가 된다.
이 특질을 사용하면 주어진 타입에 ==을 호출하거나 적절한 == 연산자가 없는 경우 예외를 던지는 TryEquals 클래스 템플릿을 만들 수 있다.
template<typename T,
bool EqComparable = IsEqualityComparable<T>::value>
struct TryEquals
{
static bool equals(T const& x1, T const& x2) {
return x1 == x2;
}
};
class NotEqualityComparable : public std::exception {};
template<typename T>
struct TryEquals<T, false>
{
static bool equals(T const& x1, T const& x2) {
throw NotEqualityComparable();
}
};
마지막으로 SpecificFunctorBridge에 TryEquals를 사용하여 저장된 함수 객체의 타입이 일치하고, 함수 객체가 ==을 지원할 때 FunctionPtr 내에서 ==을 지원하도록 할 수 있다.
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override {
if (auto const* specFb = dynamic_cast<SpecificFunctorBridge const*>(fb)) {
return TryEquals<Functor>::equals(functor, specFb->functor);
}
return false;
}
Performance Considerations
타입 삭제(type erasure)를 사용하면 정적 다형성과 동적 다형성의 장점을 동시에 제공하지만 이것이 전부는 아니다. 특히 타입 삭제를 사용하여 생성된 코드의 성능은 동적 다형성으로 생성된 코드의 성능과 거의 같다. 이는 둘 다 가상 함수를 사용한 동적 디스패치를 사용하기 때문이다. 따라서, 컴파일러가 호출을 인라인화할 수 있었던 정적 다형성의 장점이 사라진다. 이러한 성능 손실은 어플리케이션에 따라 다르지만 가상 함수 호출 비용에 비해 호출된 함수 내에서 수행되는 작업이 얼마나 되는지 비교하여 추정할 수 있다. 단순히 두 정수를 더하는 함수라면 정적 다형성을 사용하는 것보다 훨씬 더 느릴 가능성이 있다. 반면 호출된 함수 내에서 많은 작업들을 수행한다면 타입 삭제에 따른 오버로드는 측정할 수 없을 정도일 것이다.
전체 코드는 다음과 같다.
- function_ptr.h
#pragma once
#include "try_equals.h"
// forward declaration for FunctorBridge
template<typename R, typename... Args>
class FunctorBridge;
/**
* The interface to FunctionPtr is fairly straighforward, providing construction, copy, move, destruction,
* initialization, and assignment from arbitrary function objects and invocation of the underlying function
* object. The most interesting part of the interface is how it is described entirely within a class template
* partial specialization, which serves to break the template argument (a function type) into its component
* pieces (result and argument types):
*/
// primary template:
template<typename Signature>
class FunctionPtr;
// partial specialization:
template<typename R, typename... Args>
class FunctionPtr<R(Args...)>
{
private:
/**
* The implementation contains a single nonstatic member variable, bridge, which will be responsible for
* both storage and manipulation of the stored function object. Ownership of this pointer is tied to the
* FunctionPtr object, so most of the implementation provided merely manages this pointer.
*/
FunctorBridge<R, Args...>* bridge;
public:
// constructors:
FunctionPtr() : bridge(nullptr) {}
FunctionPtr(FunctionPtr const& other); // a definition is below
FunctionPtr(FunctionPtr& other) : FunctionPtr(static_cast<FunctionPtr const&>(other)) {}
FunctionPtr(FunctionPtr&& other) : bridge(other.bridge) {
other.bridge = nullptr;
}
// constructor from arbitrary function objects:
template<typename F>
FunctionPtr(F&& f); // a definition is below
// assignment operators:
FunctionPtr& operator=(FunctionPtr const& other) {
FunctionPtr tmp(other);
swap(*this, tmp);
return *this;
}
FunctionPtr& operator=(FunctionPtr&& other) {
delete bridge;
bridge = other.bridge;
other.bridge = nullptr;
return *this;
}
// construction and assignment from arbitrary function objects:
template<typename F>
FunctionPtr& operator=(F&& f) {
FunctionPtr tmp(std::forward<F>(f));
swap(*this, tmp);
return *this;
}
// destructor
~FunctionPtr() {
delete bridge;
}
friend void swap(FunctionPtr& fp1, FunctionPtr& fp2) {
std::swap(fp1.bridge, fp2.bridge);
}
explicit operator bool() const {
return bridge == nullptr;
}
// invocation:
R operator()(Args... args) const; // a definition is below
friend bool operator==(FunctionPtr const& f1, FunctionPtr const& f2) {
if (!f1 || !f2) {
return !f1 && !f2;
}
return f1.bridge->equals(f2.bridge);
}
friend bool operator!=(FunctionPtr const& f1, FunctionPtr const& f2) {
return !(f1 == f2);
}
};
/** Bridge Interface
* The FunctorBridge class template is responsible for the ownership and manipulation of the underlying
* function object. It is implemented as an abstract base class, forming the foundation for the dynamic
* polymorphism of FunctionPtr.
* FunctorBridge provides the essential operations needed to manipulate a stored function object through
* virtual functions: a destructor, a clone() operation to perform copies, and an invoke() operation to be
* const member functions.
*/
template<typename R, typename... Args>
class FunctorBridge
{
public:
virtual ~FunctorBridge() {}
virtual FunctorBridge* clone() const = 0;
virtual R invoke(Args... args) const = 0;
virtual bool equals(FunctorBridge const* fb) const = 0;
};
/**
* Using virtual function of FunctorBridge above, we can implement FunctionPtr's copy constructor and
* function call operator:
*/
template<typename R, typename... Args>
FunctionPtr<R(Args...)>::FunctionPtr(FunctionPtr const& other) : bridge(nullptr)
{
if (other.bridge) {
bridge = other.bridge->clone();
}
}
template<typename R, typename... Args>
R FunctionPtr<R(Args...)>::operator()(Args... args) const
{
return bridge->invoke(std::forward<Args>(args)...);
}
/** Type Erasure
* Each instance of FunctorBridge is an abstract class, so its derived classes are responsible for providing
* actual implementations of its virtual functions. To support the complete range of potential function objects
* - an unbounded set - we would need an unbounded member of derived classes. Fortunately, we can accomplish
* this by parameterizing the derived class on the type of the function object it stores.
*/
template<typename Functor, typename R, typename... Args>
class SpecificFunctorBridge : public FunctorBridge<R, Args...>
{
Functor functor;
public:
template<typename FunctorFwd>
SpecificFunctorBridge(FunctorFwd&& functor) : functor(std::forward<FunctorFwd>(functor)) {}
virtual SpecificFunctorBridge* clone() const override {
return new SpecificFunctorBridge(functor);
}
virtual R invoke(Args... args) const override {
return functor(std::forward<Args>(args)...);
}
virtual bool equals(FunctorBridge<R, Args...> const* fb) const override {
if (auto const* specFb = dynamic_cast<SpecificFunctorBridge const*>(fb)) {
return TryEquals<Functor>::equals(functor, specFb->functor);
}
return false;
}
};
/**
* Each instance of SpecificFunctorBridge stores a copy of the function object (whose type is Functor), which
* can be invoked, copied (by cloning the SpecificFunctorBridge), or destroyed (implicitly in the destructor).
* SpecificFunctorBridge instances are created whenever a FuncionPtr is initialized to a new function object,
* completing the FunctionPtr's constructor for arbitrary function objects.
*
* Note that while the FunctionPtr constructor itself is templated on the function object type F, that type is
* known only to the particular specialization of SpecificFunctorBridge (described by the Bridge type alias).
* Once the newly allocated Bridge instance is assigned to the data member bridge, the extra information about
* the specific type F is lost due to the derived-to-based conversion from Bridge* to FunctorBridge<R, Args...>*.
* This loss of type information explains why the term type erasure is often used to describes the technique of
* bridging between static and dynamic polymorphism.
*/
template<typename R, typename... Args>
template<typename F>
FunctionPtr<R(Args...)>::FunctionPtr(F&& f) : bridge(nullptr)
{
// It uses std::decay to produce the Functor type, which makes the inferred type F suitable for storage, for
// example, by tuning references to function types info function pointer types and removing top-level const,
// volatile, and reference types.
using Functor = std::decay_t<F>;
using Bridge = SpecificFunctorBridge<Functor, R, Args...>;
bridge = new Bridge(std::forward<F>(f));
}
- try_equals.h
#pragma once
#include <utility>
#include <type_traits>
#include <exception>
template<typename T>
class IsEqualityComparable
{
private:
static void* conv(bool); // to check convertibility to bool
template<typename U>
static std::true_type test(decltype(conv(std::declval<U const&>() == std::declval<U const&>())),
decltype(conv(!(std::declval<U const&>() == std::declval<U const&>())))
);
// fallback
template<typename U>
static std::false_type test(...);
public:
static constexpr bool value = decltype(test<T>(nullptr, nullptr))::value;
};
template<typename T,
bool EqComparable = IsEqualityComparable<T>::value>
struct TryEquals
{
static bool equals(T const& x1, T const& x2) {
return x1 == x2;
}
};
class NotEqualityComparable : public std::exception {};
template<typename T>
struct TryEquals<T, false>
{
static bool equals(T const& x1, T const& x2) {
throw NotEqualityComparable();
}
};
'프로그래밍 > C & C++' 카테고리의 다른 글
[C++] Typelists (0) | 2023.12.23 |
---|---|
[C++] 메타프로그래밍 (0) | 2023.12.16 |
[C++] Overloading on Type Properties (0) | 2023.11.25 |
[C++] Traits(특질) 구현 (3) (0) | 2023.11.24 |
[C++] 값으로 전달과 참조로 전달 (std::ref(), std::cref()) (4) | 2023.07.11 |
댓글