8

Say I have a fixed memory buffer

char *buffer; 

And I allocate my structures in that buffer using placement new

struct S
{ 
    std::tuple<int, double, char> m_data; 
    auto getRecord() 
    { 
        return m_data;
    }
};

S *newS = new(buffer + offset)S; 

I know that I'm supposed to manually call the destructor of such allocated items but if there's no bookeeping / resource management involved is it ok to omit this? In other words if the destructor of the classes using the buffer is not doing anything (similar to ~S() above) is it ok to skip this step? If that's the case can I reuse the buffer without destroying the previous tenants?

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • 1
    If you don't need to do any resource management, why does it have a destructor in the first place? – Barmar Dec 29 '16 at 18:37
  • @Barmar It will have an auto generated one by the compiler – Lorah Attkins Dec 29 '16 at 18:38
  • 4
    Take care about the `buffer + offset` address alignement. – alain Dec 29 '16 at 18:43
  • @alain Please elaborate. Do you mean that if I had two different types using the buffer I'd have to ensure sth ? Or there's a caveat for a single type as well? Any useful links? – Lorah Attkins Dec 29 '16 at 18:45
  • 1
    @Lorah: You have to make sure the pointer `buffer + offset` is properly aligned for your type. That can be a problem either if `buffer` itself isn't well aligned, or if you mix types causing `offset` to break alignment. – Ben Voigt Dec 29 '16 at 18:48
  • @BenVoigt Is this a good starting point http://stackoverflow.com/a/13249953/4224575 ? – Lorah Attkins Dec 29 '16 at 18:50
  • @LorahAttkins: That's discussing the same problem, yes. It appears to assume you already understand alignment requirements though, so I wouldn't call it a "starting" point. – Ben Voigt Dec 29 '16 at 18:52
  • @BenVoigt I'm just trying to Google myself around this. Please provide any useful link if that's possible. thanks in advance – Lorah Attkins Dec 29 '16 at 18:54
  • Generally, it is not a good idea to do your own memory management if: **(1)** you do not really need it or **(2)** you don't understand enough how it works. – Phil1970 Dec 29 '16 at 20:37
  • @Phil say I want to learn how to do this and evaluate if it's beneficial in a specific case I m working – Lorah Attkins Dec 29 '16 at 20:39
  • Assuming that your buffer is of fixed size, a `std::vector` of the same size (reserved initially) would be essentially as fast as in both case there would essentially be a single memory allocation. You won't have to figure out if it is correct not to call destructor (maybe at some point, you might want to add a destructor to `S` and it might be a nightmare to maintain your code if you have assume that it is not needed). In addition to that, depending on how you usually do your memory management, you have the risk that one would call `delete` on that pointer and cause undefined behavior. – Phil1970 Dec 29 '16 at 21:53

3 Answers3

18

The standard has a rule in section 3.8 [basic.life] that covers this:

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

Lots of experts are in agreement that "depends on the side effects produced by the destructor" is far too vague to be useful. Many interpret it as a tautology meaning "If the program has undefined behavior when the destructor side effects are not evaluated, then failing to call the destructor causes undefined behavior". See Observable behavior and undefined behavior -- What happens if I don't call a destructor?

If your type has a trivial destructor (which appears to be the case in your example), then calling it (or failing to call it) has no effect whatsoever -- calling a trivial destructor does not even end the life of the object.

The lifetime of an object o of type T ends when:

  • if T is a class type with a non-trivial destructor, the destructor call starts, or
  • the storage which the object occupies is released, or is reused by an object that is not nested within o.

That is, if T doesn't have a non-trivial destructor, the only way to end the lifetime of object o is to release or reuse its storage.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
5

Technically the destructor call is not required. Practically better safe than sorry (do call the destructor)

Paul Stelian
  • 1,381
  • 9
  • 27
  • 1
    Exactly... at some point during maintenance, someone might add a destructor to `S`... No one should write that kind of code except if he has a serious performance problem and profiling tell him that memory management is the cause and that a significant improvement can be made. – Phil1970 Dec 29 '16 at 21:57
4

In addition to Ben Voigt's answer which details when it is OK to omit the destructor call, it is important to ensure the memory is properly aligned for the type to be placed-new in it. I'll try to write it up here as requested by to OP.

This line:

S *newS = new(buffer + offset)S;

works only if the address buffer + offset is aligned properly:

3.11 Alignment
1 Object types have alignment requirements (3.9.1, 3.9.2) which place restrictions on the addresses at which an object of that type may be allocated. An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated.
[...]

buffer itself is properly aligned for any type with fundamental alignment requirement:

3.7.4.1 Allocation functions
2 [...]
The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type with a fundamental alignment requirement (3.11) and then used to access the object or array in the storage allocated
[...]

To know the alignment requirement of a type, there is alignof(type). Then there is std::max_align_t, alignof(std::max_align_t) returns the greatest alignment value of all types with fundamental alignment requirement.

There is a special case of types that require an extended alignment, to be sure your type is not one of these I would include this in your program:

static_assert(alignof(S) <= alignof(std::max_align_t),  
              "Extended alignment required for S");

Then, you just have to make sure that offset is a multiple of alignof(S).

Community
  • 1
  • 1
alain
  • 11,939
  • 2
  • 31
  • 51