C++

[C++] 자주 발생하는 표준 컨데이너 관련 Error

Elan 2023. 11. 19. 02:46

queue(410, 13): [C2280] 'solution::<lambda_1>::<lambda_1>(void)': attempting to reference a deleted function

 

원인

  • 람다의 복사 생성자가 삭제된 상태에서 발생한다.
    주로 std::priority_queue같은 컨테이너의 구현상 특정한 제약에 의해 발생한다.

  • std::priority_queue를 사용할 때 Comparer로 Lambda객체 타입을 전달할 경우 발생함.

  • C++20 이전의 표준에서는 람다를 비교자로 사용할 때 람다를 복사 또는 이동하는 과정에서 문제가 발생할 수 있다.

    std::priority_queue는 내부적으로 비교자를 복사할 수 있는데,

    C++20 이전의 표준에서는 람다는 기본적으로 복사 생성자가 삭제된 상태다.

    따라서, 람다를 직접 비교자로 사용하려고 할 때 복사 생성자를 호출할 수 없어 이 오류가 발생한다.


해결 방법

  • 이 문제를 해결하기 위한 한 가지 방법은 비교자로 함수 객체를 사용하는 것이다.

    함수 객체는 클래스나 구조체로, 필요한 비교 연산자(operator())를 오버로딩한 것을 말한다.

    이 방법은 람다와 유사한 기능을 제공하면서 복사 생성자 문제를 회피할 수 있습니다.

struct NodeComparer {
    bool operator()(const Node& a, const Node& b) const {
        return a.FCost < b.FCost;
    }
};

std::priority_queue<Node, std::vector<Node>, NodeComparer> pq;

 

  • 또 다른 방법으로는 std::function을 사용하여 람다 객체를 감싸서 사용하는 방법이 있다.
std::function<bool(const Node&, const Node&)> comp = [](const Node& a, const Node& b) { return a.FCost < b.FCost; };
std::priority_queue<Node, std::vector<Node>, decltype(comp)> pq(comp);



 unordered_set(49, 99): [C2280] 'std::_Uhash_compare<_Kty,_Hasher,_Keyeq>::_Uhash_compare(const std::_Uhash_compare<_Kty,_Hasher,_Keyeq> &)': attempting to reference a deleted function

 

원인( 스압 주의!  해결 방법만 봐도 무방)

더보기
  • 일반적으로 hasher(해시 함수)나 compaerer(비교 함수), 또는 사용자 정의 타입의 복사 생성자와 관련이 있다.

    구체적으로, 위 오류는 std::unordered_set의 내부에서 사용되는 사용자 정의 타입의 복사 생성자가 삭제되었거나 사용할 수 없어서 발생한다.

    즉, std::unordered_set은 내부적으로 해시 함수와 비교 함수를 사용하는데,
    이러한 함수들 또는 이들이 참조하는 타입이 복사 불가능한 경우에 이러한 오류가 발생할 수 있다.

    아래의 std::unordered_set의 정의부분을 살펴보면

    'public _Hash<_Uset_traits<_Kty, _Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, false>>' 를 상속받는다.
_EXPORT_STD template <class _Kty, class _Hasher = hash<_Kty>, class _Keyeq = equal_to<_Kty>,
    class _Alloc = allocator<_Kty>>
class unordered_set : public _Hash<_Uset_traits<_Kty, _Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, false>> {
    // hash table of key-values, unique keys
public:
    static_assert(!_ENFORCE_MATCHING_ALLOCATORS || is_same_v<_Kty, typename _Alloc::value_type>,
        _MISMATCHED_ALLOCATOR_MESSAGE("unordered_set<T, Hasher, Eq, Allocator>", "T"));

...

}

_Hash의 첫번째 템플릿 인자인 _Uset_traits의 템플릿 인자와 생성자를 들여다보자.(3번째 줄과 27번째 줄을 잘 살펴보자)

_STD_BEGIN
template <class _Kty, // key type (same as value type)
    class _Tr, // comparator predicate type (여기에 애초에 비교자라고 적혀 있다)
    class _Alloc, // actual allocator type (should be value allocator)
    bool _Mfl> // true if multiple equivalent keys are permitted
class _Uset_traits : public _Tr { // traits required to make _Hash behave like a set
public:
    using key_type            = _Kty;
    using value_type          = _Kty;
    using _Mutable_value_type = _Kty;
    using key_compare         = _Tr;
    using allocator_type      = _Alloc;
#if _HAS_CXX17
    using node_type = _Node_handle<_List_node<value_type, typename allocator_traits<_Alloc>::void_pointer>, _Alloc,
        _Node_handle_set_base, _Kty>;
#endif // _HAS_CXX17

    static constexpr bool _Multi    = _Mfl;
    static constexpr bool _Standard = true;

    template <class... _Args>
    using _In_place_key_extractor = _In_place_key_extract_set<_Kty, _Args...>;

    _Uset_traits() = default;

	// noexcept부분을 눈여겨보자
    explicit _Uset_traits(const _Tr& _Traits) noexcept(is_nothrow_copy_constructible_v<_Tr>) : _Tr(_Traits) {}

    using value_compare = void; // TRANSITION, remove when _Standard becomes unconditionally true

    static const _Kty& _Kfn(const value_type& _Val) noexcept {
        return _Val;
    }

    static int _Nonkfn(const value_type&) noexcept { // extract "non-key" from element value (for container equality)
        return 0;
    }
};

애초에 2번째 템플릿 인자는 비교자로 쓸거라고 주석이 달려있고,

기본 생성자 이외에 explicit 키워드가 붙은 생성자는 _Tr 타입의 객체를 복사하여 _Uset_traits 객체를  생성하는 생성자다.

nothrow 조건(is_nothrow_cop_constructible_v<_Tr>)이 충족될 때에만 예외를 던지지 않도록 보장하고 있다.

무슨말이냐하면 _Tr의 복사 생성자가 예외를 던지지 않아야 한다는 뜻이다.

즉, 아래 처럼 _Tr 타입은 명시적으로 복사 생성자를 가지고 있어야 하고, 그 복사 생성자가 noexcept여야 한다는 것이다.

struct Position {
    int Y, X;

    // 기본 생성자
    Position(int y, int x) : Y(y), X(x) {}

    // 복사 생성자
    Position(const Position& other) noexcept : Y(other.Y), X(other.X) {}
};

그러나 여전히 같은 에러가 발생할 것이다.

왜냐하면 std::unorder_set의 해시 함수와 비교자에서 문제가 발생하기 때문이다.

std::unorder_set 클래스가 상속받는 
public _Hash<_Uset_traits<_Kty, _Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, false>> 부분에서
_Uset_traits의 2번째 인자로 사용되는 _Uhash_compare쪽 살펴보자.

template <class _Kty, class _Hasher, class _Keyeq>
class _Uhash_compare
    : public _Uhash_choose_transparency<_Kty, _Hasher, _Keyeq> { // traits class for unordered containers
public:
    enum { // parameters for hash table
        bucket_size = 1 // 0 < bucket_size
    };

	// _Hasher와 _Keyeq가 nothrow로 기본 생성 가능할 때 예외를 던지지 않는다
    // conjunction_v<..., ...>는 여러 조건이 모두 true일때 true를 반환하는 메타 함수
    _Uhash_compare() noexcept(
        conjunction_v<is_nothrow_default_constructible<_Hasher>, is_nothrow_default_constructible<_Keyeq>>)
        : _Mypair(_Zero_then_variadic_args_t{}, _Zero_then_variadic_args_t{}, 0.0f) {}
        
	// _Hasher 인스턴스를 복사하여 _Uhash_compare 인스턴스를 초기화
    // _Hasher가 nothrow로 복사 생성 가능하고, _Keyeq가 nothrow로 기본 생성 가능할 때 예외를 던지지 않습니다.
    explicit _Uhash_compare(const _Hasher& _Hasharg) noexcept(
        conjunction_v<is_nothrow_copy_constructible<_Hasher>, is_nothrow_default_constructible<_Keyeq>>)
        : _Mypair(_One_then_variadic_args_t{}, _Hasharg, _Zero_then_variadic_args_t{}, 0.0f) {}

    // _Hasher와 _Keyeq 인스턴스를 모두 복사하여 _Uhash_compare 인스턴스를 초기화
    // _Hasher와 _Keyeq가 모두 nothrow로 복사 생성 가능할 때, 이 생성자는 예외를 던지지 않는다
    explicit _Uhash_compare(const _Hasher& _Hasharg, const _Keyeq& _Keyeqarg) noexcept(
        conjunction_v<is_nothrow_copy_constructible<_Hasher>, is_nothrow_copy_constructible<_Keyeq>>)
        : _Mypair(_One_then_variadic_args_t{}, _Hasharg, _One_then_variadic_args_t{}, _Keyeqarg, 0.0f) {}

	// _Uhash_compare클래스의 해시 함수로 작동한다
    // _Nothrow_hash<_Hasher, _Keyty> 표현식이 참일 때 예외를 던지지 않음
    // ※ _Nothrow_hash에 대해 후술 함
    template <class _Keyty>
    _NODISCARD size_t operator()(const _Keyty& _Keyval) const noexcept(_Nothrow_hash<_Hasher, _Keyty>) {
        // hash _Keyval to size_t value
        return static_cast<size_t>(_Mypair._Get_first()(_Keyval));
    }

	// 위와 비슷함
    template <class _Keyty1, class _Keyty2>
    _NODISCARD bool operator()(const _Keyty1& _Keyval1, const _Keyty2& _Keyval2) const
        noexcept(_Nothrow_compare<_Keyeq, _Keyty1, _Keyty2>) {
        // test if _Keyval1 NOT equal to _Keyval2
        return !static_cast<bool>(_Mypair._Myval2._Get_first()(_Keyval1, _Keyval2));
    }

    _NODISCARD float& _Get_max_bucket_size() noexcept {
        return _Mypair._Myval2._Myval2;
    }

    _NODISCARD const float& _Get_max_bucket_size() const noexcept {
        return _Mypair._Myval2._Myval2;
    }

    void swap(_Uhash_compare& _Rhs) noexcept(
        conjunction_v<_Is_nothrow_swappable<_Hasher>, _Is_nothrow_swappable<_Keyeq>>) {
        _Swap_adl(_Mypair._Get_first(), _Rhs._Mypair._Get_first());
        auto& _Lsecond = _Mypair._Myval2;
        auto& _Rsecond = _Rhs._Mypair._Myval2;
        _Swap_adl(_Lsecond._Get_first(), _Rsecond._Get_first());
        _STD swap(_Lsecond._Myval2, _Rsecond._Myval2);
    }

    _Compressed_pair<_Hasher, _Compressed_pair<_Keyeq, float>> _Mypair;
};


   그리고 위 코드에 포함된 _Nothrow_hash는 다음과 같다.

template <class _Hasher, class _Kty>
_INLINE_VAR constexpr bool _Nothrow_hash =
    noexcept(static_cast<size_t>(_STD declval<const _Hasher&>()(_STD declval<const _Kty&>())));

noexcept 조건 부분은 해시 함수 '_Hasher'가 타입 '_Kty'에 대해 예외를 던지지 않는지를 컴파일 타임에 검사한다.

declval은 C++ 표준 라이브러리의 유틸리티 함수로, 실제 객체를 생성하지 않고 특정 타입의 참조를 얻기 위해 사용된다.

이는 주로 템플릿 메타프로그래밍이나 타입 특성을 평가할 때 사용된다.

declval은 컴파일 타임에만 사용되며, 런타임에 실제 객체를 생성하지 않는다.

대신, 컴파일러에게 특정 타입의 참조를 '가정하게' 만든다.

이는 특히 해당 타입의 객체를 생성할 수 없거나 생성하기 어려운 상황에서 유용하다.

예를 들어, _STD declval<const _Hasher&>()는 다음과 같은 상황에서 사용된다:

_Hasher 타입의 객체를 실제로 생성하지 않고도, _Hasher 타입의 참조를 필요로 하는 표현식을 평가하고자 할 때.

_Hasher가 생성자가 없거나, 생성자가 private이거나, 객체 생성이 복잡한 경우에도 해당 타입의 참조를 사용할 수 있다.

따라서, declval은 타입 시스템 내에서 "만약 이 타입의 객체가 있었다면"과 같은 가정을 하게 하고, 

해당 타입의 참조를 반환하여 다양한 컴파일 타임 표현식을 가능하게 한다.

실제 런타임 코드에서는 이 함수가 호출되지 않는다.

 

해결 방법

  • Hasher(해시 함수)를 따로 함수 객체로 정의하여 Hasher로 전달한다.
struct PositionHash
{
    size_t operator()(const Position& pos) const
    {
        return std::hash<int>()(pos.Y) ^ std::hash<int>()(pos.X);
    }
};

...
std::unordered_set<Position, PositionHash> visited;




 

  unordered_map(50, 99): [C2280] 'std::_Uhash_compare<_Kty,_Hasher,_Keyeq>::_Uhash_compare(const std::_Uhash_compare<_Kty,_Hasher,_Keyeq> &)': attempting to reference a deleted function

 

원인

  • 위의 std::unordered_set 문제와 같음

해결 방법

  • 위의 std::unordered_set 문제와 같음