0

When I trace calls to the member functions of my custom allocator class, as used by an STL container, I notice some strange behavior: there is a call to my allocator class's destructor that isn't matched by any earlier constructor call. How is that possible?

Here's my code:

/*
  g++ -pedantic -W -Wall -Werror screwy_allocator_example.cc -o screwy_allocator_example
*/
#include <iostream>
#include <vector>

namespace {

template<class T>
class MyAllocator {
 public:
  using value_type = T;

  template<typename U> struct rebind { using other = MyAllocator<U>; };

  MyAllocator() {
    std::cout << "    in MyAllocator ctor(this="<<this<<")" << std::endl;
    std::cout << "    out MyAllocator ctor(this="<<this<<")" << std::endl;
  }
  ~MyAllocator() {
    std::cout << "    in MyAllocator dtor(this="<<this<<")" << std::endl;
    std::cout << "    out MyAllocator dtor(this="<<this<<")" << std::endl;
  }

  template<typename From>
  MyAllocator(const From &from) {
    std::cout << "    in MyAllocator copy ctor("
              << "this="<<this<<", &from="<<(void*)&from<<")" << std::endl;
    std::cout << "    out MyAllocator copy ctor("
              << "this="<<this<<", &from="<<(void*)&from<<")" << std::endl;
  }
  template<typename From>
  MyAllocator &operator=(const From &from) {
    std::cout << "    in MyAllocator operator=("
              << "this="<<this<<", &from="<<(void*)&from<<")" << std::endl;
    std::cout << "    out MyAllocator operator=("
              << "this="<<this<<", &from="<<(void*)&from<<")" << std::endl;
    return *this;
  }

  T *allocate(size_t n) {
    std::cout << "    in MyAllocator::allocate("
              << "this="<<this<<", n="<<n<<")" << std::endl;
    // assume n*sizeof(T) doesn't overflow
    T *answer = (T*)std::malloc(n * sizeof(T));
    // assume answer!=nullptr
    std::cout << "    out MyAllocator::allocate(this="<<this<<", n="<<n<<")"
              << ", returning "<<(void*)answer << std::endl;
    return answer;
  }

  void deallocate(T *p, size_t n) {
    std::cout << "    in MyAllocator::deallocate("
              << "this="<<this<<", p="<<(void*)p<<", n="<<n<<")" << std::endl;
    std::free(p);
    std::cout << "    out MyAllocator::deallocate("
              << "this="<<this<<", p="<<(void*)p<<", n="<<n<<")" << std::endl;
  }

  template<typename U> bool operator==(const MyAllocator<U> &) const
  { return true; }
  template<typename U> bool operator!=(const MyAllocator<U> &) const
  { return false; }
};  // class MyAllocator<T>

}  // namespace

int main(const int, char**const) {
  std::cout << "in main" << std::endl;
  {
    std::cout << "  constructing my_allocator" << std::endl;
    MyAllocator<double> my_allocator;
    std::cout << "  constructed my_allocator" << std::endl;
    {
      std::cout << "  constructing v(my_allocator)" << std::endl;
      std::vector<double, MyAllocator<double>> v(my_allocator);
      std::cout << "  constructed v(my_allocator)" << std::endl;
      std::cout << "  pushing one item onto v" << std::endl;
      v.push_back(3.14);
      std::cout << "  pushed one item onto v" << std::endl;
      std::cout << "  destructing v(my_allocator)" << std::endl;
    }
    std::cout << "  destructed v(my_allocator)" << std::endl;
    std::cout << "  destructing my_allocator" << std::endl;
  }
  std::cout << "  destructed my_allocator" << std::endl;
  std::cout << "out main" << std::endl;
  return 0;
}

Here's the output (it's the same for -std=c++11, -std=c++14, -std=c++17, -std=c++2a):

in main
  constructing my_allocator
    in MyAllocator ctor(this=0x7ffe4cee5747)
    out MyAllocator ctor(this=0x7ffe4cee5747)
  constructed my_allocator
  constructing v(my_allocator)
  constructed v(my_allocator)
  pushing one item onto v
    in MyAllocator::allocate(this=0x7ffe4cee5720, n=1)
    out MyAllocator::allocate(this=0x7ffe4cee5720, n=1), returning 0x55890a41d2c0
  pushed one item onto v
  destructing v(my_allocator)
    in MyAllocator::deallocate(this=0x7ffe4cee5720, p=0x55890a41d2c0, n=1)
    out MyAllocator::deallocate(this=0x7ffe4cee5720, p=0x55890a41d2c0, n=1)
    in MyAllocator dtor(this=0x7ffe4cee5720)
    out MyAllocator dtor(this=0x7ffe4cee5720)
  destructed v(my_allocator)
  destructing my_allocator
    in MyAllocator dtor(this=0x7ffe4cee5747)
    out MyAllocator dtor(this=0x7ffe4cee5747)
  destructed my_allocator
out main

Notice that there were two calls to the dtor, but only one call to the ctor:

  • first the MyAllocator ctor got called, with this=0x7ffe4cee5747
  • then a second mystery MyAllocator object appeared at this=0x7ffe4cee5720, was used, and then was destructed (without ever having been constructed!?)
  • then the first MyAllocator object at this=0x7ffe4cee5747 (the one that was constructed) gets destructed as expected.

What's going on here?

It seems the simplest explanation would be that I'm just forgetting about some flavor of constructor that the compiler generates for me. Is that it?

If not, here are some other thoughts.

I know that prior to c++11, allocator objects were required to be "stateless". I'm not sure precisely what that means, but perhaps it could be argued that it implies that, prior to c++11, the compiler could be justified in playing games like making bytewise copies of allocator objects, skipping the constructor calls?

But, regardless, in c++11 and later, allocators are supposedly allowed to have state, right? Given that, I don't see how it can make any sense for the constructor call to be skipped.

Given this strange behavior, it seems to me that I have no choice but to implement my allocator using the pre-c++11 restrictions: that is, the allocator must be nothing more than a stateless handle to some other resource. That's what I'm doing, successfully, but I'd like to understand why this is necessary.

Don Hatch
  • 5,041
  • 3
  • 31
  • 48
  • Short answer: You didn't provide a normal copy constructor/assignment, just the converting ones. The compiler defaulted them, and that's why you don't see that copy. – Dave S Apr 04 '20 at 13:38
  • Also implement move constructor using `MyAllocator(From && from)`. I see some calls to move constructor as well. – TonySalimi Apr 04 '20 at 13:44
  • @DaveS Yes, adding a "normal" copy constructor fixes it. Thank you! But, I'm surprised that the class itself (MyAllocator) didsn't match the template parameter of my converting constructor. Is this some special case that I didn't know about? Is there a reference for it? – Don Hatch Apr 04 '20 at 13:48
  • @Gupta If I understand correctly, the move constructor is optional; the compiler will not generate a default one of those, will it? – Don Hatch Apr 04 '20 at 13:51
  • Does this answer your question? [C++ template copy constructor on template class](https://stackoverflow.com/questions/32537994/c-template-copy-constructor-on-template-class) (Note that move constructors work the same way as copy constructors in this aspect - default versions are generated if conditions are met and no user-defined ones are provided) – L. F. Apr 04 '20 at 14:00
  • @donhatch Move constructors are generated in specific cases. Chech: https://stackoverflow.com/questions/8283589/are-move-constructors-produced-automatically – TonySalimi Apr 04 '20 at 19:24
  • @L.F. Yes, perfectly. Thank you. – Don Hatch Apr 04 '20 at 20:52
  • @L.F. You also say "Note that move constructors work the same way as copy constructors in this aspect - default versions are generated if conditions are met and no user-defined ones are provided". Ok, good to know. That means that, once I've provided the copy constructor, it will be no longer be the case that "conditions are met"; i.e. I need not provide a move constructor if my goal is to handle all constructions and destructions. – Don Hatch Apr 04 '20 at 21:01
  • @DonHatch You don't need to provide a move constructor if you already provide a copy constructor and moving has no advantage compared to copying, yes. – L. F. Apr 05 '20 at 02:05

0 Answers0