Especialização std :: alocador fornecida pelo usuário


8

Os modelos de classe no ::stdnamespace geralmente podem ser especializados por programas para tipos definidos pelo usuário. Não encontrei nenhuma exceção a esta regra para std::allocator.

Então, posso me especializar std::allocatorpara meus próprios tipos? E se me for permitido, preciso fornecer todos os membros do std::allocatormodelo primário de, já que muitos deles podem ser fornecidos por std::allocator_traits(e, consequentemente, obsoletos no C ++ 17)?

Considere este programa

#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();
}

A saída deste programa com libc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++) é:

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

e a saída com libstdc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++) é:

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

Como você pode ver libstdc ++ nem sempre honrar a sobrecarga do constructque eu forneci e se eu remover os construct, destroyou max_sizemembros, em seguida, o programa nem sequer compilar com libstdc ++ reclamando sobre esses membros desaparecidos, embora eles são fornecidos pela std::allocator_traits.

O programa tem um comportamento indefinido e, portanto, as duas bibliotecas padrão estão corretas ou o comportamento do programa é bem definido e a biblioteca padrão necessária para usar minha especialização?


Note que existem alguns membros do std::allocatormodelo principal que ainda deixei de fora na minha especialização. Preciso adicioná-los também?

Para ser preciso, eu deixei de fora

using is_always_equal = std::true_type

que é fornecido pelo fato de std::allocator_traitsmeu alocador estar vazio, mas faria parte da std::allocatorinterface de.

Também deixado de fora pointer, const_pointer, reference, const_reference, rebinde address, os quais são fornecidos por std::allocator_traitse preterido em C ++ para 17 std::allocatorde interface.

Se você acha que é necessário definir todos esses itens para corresponder std::allocatorà interface, considere-os adicionados ao código.


Eu retiro tudo isso. Eu tentei no Visual Studio 2019 e possui todas as chamadas do construtor (mesmo o construtor de cópia). . No entanto, acho que o libstdc ++ o implementa de uma maneira que o otimizador pensa que pode removê-los.
Spencer

Respostas:


3

De acordo com 23.2.1 [container.requirements.general] / 3:

Para os componentes afetados por esta subcláusula que declaram um allocator_type, os objetos armazenados nesses componentes devem ser construídos usando a allocator_traits<allocator_type>::constructfunção

Além disso, de acordo com 17.6.4.2.1:

O programa pode adicionar uma especialização de modelo para qualquer modelo de biblioteca padrão ao namespace stdapenas se a declaração depender de um tipo definido pelo usuário e a especialização atender aos requisitos de biblioteca padrão do modelo original e não for explicitamente proibida.

Eu não acho que o padrão proíba a especialização std::allocator, já que li todas as seções std::allocatore não mencionou nada. Também examinei como é o padrão proibir a especialização e não encontrei nada parecido std::allocator.

Os requisitos para Allocatorestão aqui , e sua especialização os satisfaz.

Portanto, só posso concluir que o libstdc ++ realmente viola o padrão (talvez eu tenha cometido um erro em algum lugar). Eu descobri que, se alguém simplesmente se especializar std::allocator, o libstdc ++ responderá usando a colocação new para o construtor, porque eles têm uma especialização de modelo especificamente para esse caso, enquanto usam o alocador especificado para outras operações; o código relevante está aqui (está aqui namespace std; allocatoraqui está ::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_nchamadas std::_Constructque usam posicionamento novo. Isso explica por que você não vê "Construindo um" antes de "Alocando para 4" em sua saída.

EDIT: Como OP apontou em um comentário, std::__uninitialized_default_nchama

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

que na verdade tem uma especialização se __is_trivial(_ValueType) && __assignablefor true, que está aqui . Ele usa std::fill_n(onde valueé trivialmente construído) em vez de chamar std::_Constructcada elemento. Como Aé trivial e pode ser atribuído a uma cópia, acabaria chamando essa especialização. Obviamente, isso também não é usado std::allocator_traits<allocator_type>::construct.


1
No meu caso específico, na verdade, ele não está usando a std::_Constructchamada do que posso dizer, porque Aé trivialmente copiável e trivialmente atribuível à cópia; portanto, __uninitialized_default_n_1é escolhida a outra especialização , que chama std::fill_n, em vez disso, que envia novamente para memcpy/ memset. Eu sabia disso como uma otimização que libstdc ++ faz, mas não percebi que a std::allocator::constructchamada também é ignorada para tipos não triviais. Portanto, pode ser apenas uma supervisão de como o libstdc ++ identifica que a biblioteca fornecida std::allocatoré usada.
walnut

Você está certo. Editado.
Leonid
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.