Std :: phân bổ chuyên môn do người dùng cung cấp


8

Các mẫu lớp trong ::stdkhông gian tên thường có thể được các chương trình dành riêng cho các kiểu do người dùng định nghĩa. Tôi không tìm thấy bất kỳ ngoại lệ cho quy tắc này cho std::allocator.

Vì vậy, tôi có được phép chuyên môn hóa std::allocatorcho các loại của riêng tôi? Và nếu tôi được phép, tôi có cần cung cấp cho tất cả các thành viên của std::allocatormẫu chính, với điều kiện là nhiều trong số họ có thể được cung cấp bởi std::allocator_traits(và do đó không được chấp nhận trong C ++ 17)?

Xem xét chương trình này

#include<vector>
#include<utility>
#include<type_traits>
#include<iostream>
#include<limits>
#include<stdexcept>

struct A { };

namespace std {
    template<>
    struct allocator<A> {
        using value_type = A;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
        using propagate_on_container_move_assignment = std::true_type;

        allocator() = default;

        template<class U>
        allocator(const allocator<U>&) noexcept {}

        value_type* allocate(std::size_t n) {
            if(std::numeric_limits<std::size_t>::max()/sizeof(value_type) < n)
                throw std::bad_array_new_length{};
            std::cout << "Allocating for " << n << "\n";
            return static_cast<value_type*>(::operator new(n*sizeof(value_type)));
        }

        void deallocate(value_type* p, std::size_t) {
            ::operator delete(p);
        }

        template<class U, class... Args>
        void construct(U* p, Args&&... args) {
            std::cout << "Constructing one\n";
            ::new((void *)p) U(std::forward<Args>(args)...);
        };

        template<class U>
        void destroy( U* p ) {
            p->~U();
        }

        size_type max_size() const noexcept {
            return std::numeric_limits<size_type>::max()/sizeof(value_type);
        }
    };
}

int main() {
    std::vector<A> v(2);
    for(int i=0; i<6; i++) {
        v.emplace_back();
    }
    std::cout << v.size();
}

Đầu ra của chương trình này với libc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++) là:

Allocating for 2
Constructing one
Constructing one
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

và đầu ra với libstdc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++) là:

Allocating for 2
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

Như bạn có thể thấy libstdc ++ không phải lúc nào cũng tôn vinh sự quá tải constructmà tôi đã cung cấp và nếu tôi loại bỏconstruct , destroyhoặc max_sizecác thành viên, thì chương trình thậm chí không biên dịch với libstdc ++ phàn nàn về những thành viên bị mất này, mặc dù chúng được cung cấp bởi std::allocator_traits.

Chương trình có hành vi không xác định và do đó cả hai thư viện chuẩn đều đúng hay hành vi của chương trình được xác định rõ và thư viện chuẩn cần có để sử dụng chuyên môn của tôi?


Lưu ý rằng có một số thành viên từ std::allocator mẫu chính mà tôi vẫn bỏ quên trong chuyên môn của mình. Tôi có cần thêm chúng không?

Nói chính xác, tôi đã bỏ đi

using is_always_equal = std::true_type

được cung cấp bởi std::allocator_traits vì bộ cấp phát của tôi trống, nhưng sẽ là một phần của std::allocatorgiao diện.

Tôi cũng bỏ qua pointer, const_pointer, reference, const_reference, rebindaddress, tất cả đều được cung cấp bởistd::allocator_traits và bị phản đối trong C ++ 17 std::allocator's giao diện.

Nếu bạn nghĩ rằng cần phải xác định tất cả những điều này để khớp với std::allocatorgiao diện, thì vui lòng xem xét chúng được thêm vào mã.


Tôi lấy lại tất cả Tôi đã thử nó trong Visual Studio 2019 và nó có tất cả các lệnh gọi của hàm tạo (ngay cả các lệnh gọi hàm tạo sao chép). . Tuy nhiên, tôi đoán libstdc ++ thực hiện nó theo cách mà trình tối ưu hóa nghĩ rằng nó có thể loại bỏ chúng.
Spencer

Câu trả lời:


3

Theo 23.2.1 [container.requirements.general] / 3:

Đối với các thành phần bị ảnh hưởng bởi điều khoản này khai báo một allocator_type, các đối tượng được lưu trữ trong các thành phần này sẽ được xây dựng bằng cách sử dụng allocator_traits<allocator_type>::constructhàm

Ngoài ra, theo 17.6.4.2.1:

Chương trình có thể thêm chuyên môn hóa mẫu cho bất kỳ mẫu thư viện tiêu chuẩn nào vào không gian tên stdnếu khai báo phụ thuộc vào loại do người dùng xác định và chuyên môn đáp ứng các yêu cầu thư viện chuẩn cho mẫu gốc và không bị cấm rõ ràng.

Tôi không nghĩ rằng tiêu chuẩn cấm chuyên biệt std::allocator, vì tôi đã đọc qua tất cả các phần trên std::allocatorvà nó không đề cập đến bất cứ điều gì. Tôi cũng đã xem xét tiêu chuẩn trông như thế nào để cấm chuyên môn hóa và tôi đã không tìm thấy bất cứ điều gì giống như vậy std::allocator.

Các yêu cầu cho đâyAllocator là , và chuyên môn của bạn đáp ứng chúng.

Do đó, tôi chỉ có thể kết luận rằng libstdc ++ thực sự vi phạm tiêu chuẩn (có lẽ tôi đã mắc lỗi ở đâu đó). Tôi thấy rằng nếu chỉ đơn giản là chuyên môn std::allocator, libstdc ++ sẽ phản hồi bằng cách sử dụng vị trí mới cho hàm tạo vì chúng có chuyên môn mẫu đặc biệt cho trường hợp này trong khi sử dụng bộ cấp phát được chỉ định cho các hoạt động khác; mã có liên quan ở đây (đây là namespace std; allocatorđây là ::std::allocator):

  // __uninitialized_default_n_a
  // Fills [first, first + n) with n default constructed value_types(s),
  // constructed with the allocator alloc.
  template<typename _ForwardIterator, typename _Size, typename _Allocator>
    _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                _Allocator& __alloc)
    {
      _ForwardIterator __cur = __first;
      __try
    {
      typedef __gnu_cxx::__alloc_traits<_Allocator> __traits;
      for (; __n > 0; --__n, (void) ++__cur)
        __traits::construct(__alloc, std::__addressof(*__cur));
      return __cur;
    }
      __catch(...)
    {
      std::_Destroy(__first, __cur, __alloc);
      __throw_exception_again;
    }
    }

  template<typename _ForwardIterator, typename _Size, typename _Tp>
    inline _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                allocator<_Tp>&)
    { return std::__uninitialized_default_n(__first, __n); }

std::__uninitialized_default_ncác cuộc gọi std::_Constructsử dụng vị trí mới. Điều này giải thích lý do tại sao bạn không thấy "Xây dựng một" trước khi "Phân bổ cho 4" trong đầu ra của bạn.

EDIT: Như OP đã chỉ ra trong một bình luận, std::__uninitialized_default_ncác cuộc gọi

__uninitialized_default_n_1<__is_trivial(_ValueType)
                             && __assignable>::
__uninit_default_n(__first, __n)

mà thực sự có một chuyên môn nếu __is_trivial(_ValueType) && __assignabletrue, đó là ở đây . Nó sử dụng std::fill_n(nơi valueđược xây dựng tầm thường) thay vì gọi std::_Constructtừng phần tử. Vì Alà tầm thường và sao chép có thể gán, nên cuối cùng nó sẽ gọi chuyên ngành này. Tất nhiên, điều này cũng không sử dụng std::allocator_traits<allocator_type>::construct.


1
Trong trường hợp cụ thể của tôi, nó không thực sự sử dụng std::_Constructcuộc gọi từ những gì tôi có thể nói, bởi vì có thể Asao chép một cách tầm thường và sao chép có thể gán được, do đó, chuyên môn khác __uninitialized_default_n_1được chọn, std::fill_nthay vào đó, sau đó lại gửi đến memcpy/ memset. Tôi đã nhận thức được điều này như một libstdc ++ tối ưu hóa tạo ra, nhưng tôi đã không nhận ra rằng std::allocator::constructcuộc gọi cũng bị bỏ qua cho các loại không tầm thường. Vì vậy, nó có thể chỉ là một sự giám sát trong cách libstdc ++ xác định rằng thư viện cung cấp std::allocatorđược sử dụng.
quả óc chó

Bạn đúng. Đã chỉnh sửa.
Leonid
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.