Skip to the content.

[C++20] Concepts

C++20에서 concenpt 개념이 나왔다. Swift로 치면 Generic 같은거라 보면 될듯. std에서 기본적으로 제공하는 몇가지 concept들을 다뤄보려고 한다. 링크

std::same_as

Type 비교에 쓰이는 concept

std::same_as은 concept이고 std::is_same는 struct 형태다.

아래처럼 foo 함수를 두 개 만들어보자.

template<class T, class U> requires (std::is_same<T, U>::value)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires ((std::is_same<T, U>::value) && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2); // ERROR: Call to 'foo' is ambiguous
    return 0;
}

그리고 main에서 foo<int, int>(1, 2)를 호출할 경우, int는 숫자이기에 당연히 두번째 함수가 불릴 것일 예상하겠지만 compiler는 ambiguous하다는 오류를 내뿜는다.

이유는 단순하다. T = int, U = int는 두 함수의 조건을 모두 만족하니까. 두번째 함수가 불리게 하려면 std::is_same 대신 std::same_as를 쓰면 된다.

template<class T, class U> requires (std::same_as<T, U>)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires (std::same_as<T, U> && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2);
    return 0;
}

이 현상이 나타나는 이유는 concept를 활용하면 constraint가 생성되고 이는 compiler가 overloading 할 떄 도움이 되는 지표이기 때문이다.

std::same_as는 두 번 비교한다.

same_as.h를 보면 std::same_as는 아래처럼 정의된다.

template<class _Tp, class _Up>
concept __same_as_impl = _IsSame<_Tp, _Up>::value;

template<class _Tp, class _Up>
concept same_as = __same_as_impl<_Tp, _Up> && __same_as_impl<_Up, _Tp>;

T, U이라는 type이 들어오면 T == U, U == T를 모두 확인한다.

… T == U만 확인해도 되는 것 아닌가? 라는 생각이 들 것이다. 그렇지 않은 이유를 설명하기 위해, std::same_as 같은 concept를 직접 만들어보자

template<class T, class U>
concept my_same_as = std::is_same<T, U>::value;

template<class T, class U> requires (my_same_as<T, U>)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires (my_same_as<T, U> && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2);
    return 0;
}

위 코드에서는 T == U만 비교해도 문제가 없다. 이제 두번째 함수의 T와 U를 바꿔보자

template<class T, class U>
concept my_same_as = std::is_same<T, U>::value;

template<class T, class U> requires (my_same_as<T, U>)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires (my_same_as<U, T> && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2); // ERROR: Call to 'foo' is ambiguous
    return 0;
}

ambiguous 오류가 뜬다. my_same_as<T, U>my_same_as<U, T>은 서로 다른 constraint를 가지고 있기 때문이기에 상기한 compiler 로직이 작동되지 않아서 ambiguous 오류가 뜨게 된다.

위 코드가 작동되게 하려면 my_same_as<T, U>my_same_as<U, T>은 서로 같은 constraint를 갖게 해야 한다. 아래처럼 말이다.

template<class T, class U>
concept __my_same_as_impl = std::is_same<T, U>::value;

template<class T, class U>
concept my_same_as = __my_same_as_impl<T, U> && __my_same_as_impl<U, T>;

template<class T, class U> requires (my_same_as<T, U>)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires (my_same_as<U, T> && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2);
    return 0;
}

__my_same_as_impl라는 concept를 새로 추가했고, my_same_as에서 T == U, U == T를 비교하게 했더니 문제가 없다. 이렇게 하면 my_same_as<T, U>와 my_same_as<U, T>는 같은 constraint를 갖게 되기 때문이다.

같은 constraint를 갖는 이유는, __my_same_as_impl<T, U>를 A라는 constraint라고 하고 __my_same_as_impl<U, T>를 B라는 constraint라고 가정하자

그러면 my_same_as<T, U>은 A && B가 되고, my_same_as<U, T>은 B && A다. 같은 constraint가 형성된다.

하지만 __my_same_as_impl은 왜 있어야 하는 것인가? 라는 의문이 가질 수 있다. 한 번 __my_same_as_impl 없이 해보자

template<class T, class U>
concept my_same_as = std::is_same<T, U>::value && std::is_same<U, T>::value;

template<class T, class U> requires (my_same_as<T, U>)
auto foo(T a, T b) { return a + b; };

template<class T, class U> requires (my_same_as<U, T> && std::integral<T>)
auto foo(T a, T b) { return a + b; };


int main(int argc, const char * argv[]) {
    foo<int, int>(1, 2); // ERROR: Call to 'foo' is ambiguous
    return 0;
}

ambiguous 오류가 뜬다. 이유는 std::is_same은 concept가 아니기 때문에, my_same_as<T, U>와 my_same_as<U, T>는 서로 다른 constraint를 지니기 때문에 오류가 뜬다.

std::convertible_to, std::common_reference_with, std::common_with

cppreference에서는 각각 아래처럼 설명한다.

std::convertible_to은 쉽게 이해가고, std::common_reference_with과 std::common_with은 두 type의 공통점을 찾아 주는데, 그게 reference type인지 아닌지에 따라 다른 것 같다… 인데 이해는 가는데 와닿지는 않는다.

각각 하나씩 살펴보자

std::convertible_to

class에서 public 상속 관계일 경우 아래처럼 활용할 수 있다. 당연히 protected/private 상속일 떄는 불가능하다. 이거는 dynamic_cast에서도 마찬가지

class MyObjectA {};
class MyObjectB: public MyObjectA {};

int main(int argc, const char * argv[]) {
    if constexpr(std::convertible_to<MyObjectB, MyObjectA>) {
        std::printf("True\n");
    }
    
    MyObjectB *b = new MyObjectB {};
    if (dynamic_cast<MyObjectA *>(b)) {
        std::printf("Casted\n");
    }
    delete b;
    
    return 0;
}

operator 관계일 경우에도 가능하다. operator는 public이어야 한다. 참고로 dynamic_cast는 operator 관계를 판별하지 못한다.

class MyObjectA {};
class MyObjectB {
public:
    operator MyObjectA() { return {}; };
};

int main(int argc, const char * argv[]) {
    if constexpr(std::convertible_to<MyObjectB, MyObjectA>) {
        std::printf("True\n");
    }
    
    return 0;
}

std::common_reference_with

두 type이 공통 참조 관계에 있는지 확인한다. std::convertible_to는 순서가 중요하지만 std::common_reference_with은 순서가 중요하지 않다.

class MyObjectA {};
class MyObjectB {
public:
    operator MyObjectA() const { return {}; };
};

int main(int argc, const char * argv[]) {
    if constexpr(std::common_reference_with<MyObjectA&, const MyObjectB&>) {
        std::printf("True\n");
    }
    
    return 0;
}

만약 operator에 const 키워드가 없다면 위는 성립하지 않는다.

위 코드는 operator 관계일 때인데 class 상속 관계에서도 성립한다.

std::common_with

두 type의 값 공통 관계에 있는지 확인한다.

class MyObjectA {};
class MyObjectB {
public:
    operator MyStructA() const { return {}; };
};

int main(int argc, const char * argv[]) {
    if constexpr(std::common_with<MyObjectA, const MyObjectB>) {
        std::printf("True\n");
    }
    
    return 0;
}

만약 operator에 const 키워드가 없다면 위는 성립하지 않는다.

위 코드는 operator 관계일 때인데 class 상속 관계에서도 성립한다.

마치며

concept는 Swift의 Generic 개념과 같네! 별거 없네!

라는 가벼운 마음으로 공부하다가 전혀 그렇지 않다는 것을 느꼈다 😥

사실 Swift도 내부적으로 비슷하게 동작할거다. 하지만 개발자에게 이런 micro control을 요구하지 않을 뿐

Objective-C에서도 Generic 기능이 있긴 한데 그냥 없다시피한 기능이고 내부 동작도 다르기에, concept와 Objective-C++ 상호 호환성은 없어 보인다.

참고하면 좋은 글

Why does same_as concept check type equality twice?