1

I've been trying not to initialize memory when I don't need to, and am using malloc arrays to do so:

This is what I've run:

#include <iostream>

struct test
{
    int num = 3;

    test() { std::cout << "Init\n"; }
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

int main()
{
    test* array = (test*)malloc(3 * sizeof(test));

    for (int i = 0; i < 3; i += 1)
    {
        std::cout << array[i].num << "\n";
        array[i].num = i;
        //new(array + i) i; placement new is not being used
        std::cout << array[i].num << "\n";
    }

    for (int i = 0; i < 3; i += 1)
    {
        (array + i)->~test();
    }

    free(array);

    return 0;
}

Which outputs:

0 ->- 0
0 ->- 1
0 ->- 2
Destroyed: 0
Destroyed: 1
Destroyed: 2

Despite not having constructed the array indices. Is this "healthy"? That is to say, can I simply treat the destructor as "just a function"? (besides the fact that the destructor has implicit knowledge of where the data members are located relative to the pointer I specified)

Just to specify: I'm not looking for warnings on the proper usage of c++. I would simply like to know if there's things I should be wary of when using this no-constructor method.

(footnote: the reason I don't wanna use constructors is because many times, memory simply does not need to be initialized and doing so is slow)

ZeroZ30o
  • 375
  • 2
  • 18
  • In `0 ->- 0` don't count on that first 0 being 0. You've got a dose of UB going on here. – user4581301 Jul 04 '19 at 01:29
  • @user4581301 I do realize it really is UB ->- 0, and UB ->- 1, and UB ->- 2. That is not an issue. – ZeroZ30o Jul 04 '19 at 01:30
  • 2
    `test* array = (test*)malloc(3 * sizeof(test));` -- You have not constructed anything here. No objects exist. – PaulMcKenzie Jul 04 '19 at 01:31
  • See [this answer](https://stackoverflow.com/questions/2995099/malloc-and-constructors). The constructor isn't called if you use `malloc()`, only if you use `new`. – jbinvnt Jul 04 '19 at 01:31
  • @PaulMcKenzie I'm aware. I specifically stated in the post that I am aware of this fact. I even have "placement new is not being used" shown as a comment just in case. That's not what I'm asking. – ZeroZ30o Jul 04 '19 at 01:34
  • 1
    The only reason why you've gotten this far in your code is that C++ syntax allows you to do what you're doing. It is still invalid. – PaulMcKenzie Jul 04 '19 at 01:35
  • If you're just dealing with POD types, then it's not any different than allocating (and deallocating) C `struct`s: you don't need to have and to use constructors or destructors. But if your `struct` has members that also have constructors and destructors, then you'll want to properly use the constructor *and* destructor on your containing class too; you really don't want to have to deal with constructing and destructing each member (and dealing with ordering, exceptions, etc.). Either way it doesn't make sense to do one without the other. – jamesdlin Jul 04 '19 at 01:36
  • If you want to "avoid the slow" in this example then change `int num = 3;` to `int num;` and only assign the 3 in cases where you want the assignment. (And don't read from `num` if you didn't give it a value yet). – M.M Jul 04 '19 at 01:38
  • @jamesdlin it doesn't make sense in this context because I do num = i, but in a case where I simply do not initialize at construction time (because it's not needed yet) and I destruct it later (after modifying it when it need be changed), it would be benefitial. – ZeroZ30o Jul 04 '19 at 01:40
  • @ZeroZ30o No, it never makes sense to use a destructor without a constructor. Either use both of them or neither of them. If you want to construct and destruct objects *lazily*, then just do that directly without these other shenanigans. – jamesdlin Jul 04 '19 at 01:47
  • You can't modify a non-POD object or even assign to it before you construct it. If you want to assign some value to it later, you would use the copy constructor. – eesiraed Jul 04 '19 at 03:49

4 Answers4

6

No, this is undefined behaviour. An object's lifetime starts after the call to a constructor is completed, hence if a constructor is never called, the object technically never exists.

This likely "seems" to behave correctly in your example because your struct is trivial (int::~int is a no-op).

You are also leaking memory (destructors destroy the given object, but the original memory allocated via malloc still needs to be freed).

Edit: You might want to look at this question as well, as this is an extremely similar situation, simply using stack allocation instead of malloc. This gives some of the actual quotes from the standard around object lifetime and construction.

I'll add this as well: in the case where you don't use placement new and it clearly is required (e.g. struct contains some container class or a vtable, etc.) you are going to run into real trouble. In this case, omitting the placement-new call is almost certainly going to gain you 0 performance benefit for very fragile code - either way, it's just not a good idea.

Yuushi
  • 25,132
  • 7
  • 63
  • 81
3

Yes, the destructor is nothing more than a function. You can call it at any time. However, calling it without a matching constructor is a bad idea.

So the rule is: If you did not initialize memory as a specific type, you may not interpret and use that memory as an object of that type; otherwise it is undefined behavior. (with char and unsigned char as exceptions).

Let us do a line by line analysis of your code.

test* array = (test*)malloc(3 * sizeof(test));

This line initializes a pointer scalar array using a memory address provided by the system. Note that the memory is not initialized for any kind of type. This means you should not treat these memory as any object (even as scalars like int, let aside your test class type).

Later, you wrote:

std::cout << array[i].num << "\n";

This uses the memory as test type, which violates the rule stated above, leading to undefined behavior.

And later:

(array + i)->~test();

You used the memory a test type again! Calling destructor also uses the object ! This is also UB.

In your case you are lucky that nothing harmful happens and you get something reasonable. However UBs are solely dependent on your compiler's implementation. It can even decide to format your disk and that's still standard-conforming.

ph3rin
  • 4,426
  • 1
  • 18
  • 42
  • 2
    Note that the undefined behaviour starts with `std::cout << array[i].num << "\n"` (you can not try to access an object that doesn't exist); the destructor call is just icing on the cake as it were – M.M Jul 04 '19 at 01:56
  • I do know about security and instruction "injections", my argument is more "while the object is not constructed (UB) it is later properly initialized before use (same as if I had constructed it from the start). – ZeroZ30o Jul 04 '19 at 02:13
  • @ZeroZ30o As long as you use the object after it is constructed it is ok. But don't use it when it is possibly not constructed. – ph3rin Jul 04 '19 at 02:23
  • @ZeroZ30o How are you initializing it then? If your object had something like a vector as a member, trying to assign to the vector without constructing it would be UB. Also, you need to understand that UB is not just some random value. Invoking undefined behavior voids your entire program. If you try to read something uninitialized, anything can happen, including lots of weird crap besides just getting garbage values. – eesiraed Jul 04 '19 at 03:57
1

That is to say, can I simply treat the destructor as "just a function"?

No. While it is like other functions in many ways, there are some special features of the destructor. These boil down to a pattern similar to manual memory management. Just as memory allocation and deallocation need to come in pairs, so do construction and destruction. If you skip one, skip the other. If you call one, call the other. If you insist upon manual memory management, the tools for construction and destruction are placement new and explicitly calling the destructor. (Code that uses new and delete combine allocation and construction into one step, while destruction and deallocation are combined into the other.)

Do not skip the constructor for an object that will be used. This is undefined behavior. Furthermore, the less trivial the constructor, the more likely that something will go wildly wrong if you skip it. That is, as you save more, you break more. Skipping the constructor for a used object is not a way to be more efficient — it is a way to write broken code. Inefficient, correct code trumps efficient code that does not work.

One bit of discouragement: this sort of low-level management can become a big investment of time. Only go this route if there is a realistic chance of a performance payback. Do not complicate your code with optimizations simply for the sake of optimizing. Also consider simpler alternatives that might get similar results with less code overhead. Perhaps a constructor that performs no initializations other than somehow flagging the object as not initialized? (Details and feasibility depend on the class involved, hence extend outside the scope of this question.)

One bit of encouragement: If you think about the standard library, you should realize that your goal is achievable. I would present vector::reserve as an example of something that can allocate memory without initializing it.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
0

You currently have UB as you access field from non-existing object.

You might let field uninitialized by doing a constructor noop. compiler might then easily doing no initialization, for example:

struct test
{
    int num; // no = 3

    test() { std::cout << "Init\n"; } // num not initalized
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

Demo

For readability, you should probably wrap it in dedicated class, something like:

struct uninitialized_tag {};

struct uninitializable_int
{
    uninitializable_int(uninitialized_tag) {} // No initalization
    uninitializable_int(int num) : num(num) {}

    int num;
};

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302