104

I was told that the following code has undefined behavior until C++20:

int *p = (int*)malloc(sizeof(int));
*p = 10;

Is that true?

The argument was that the lifetime of the int object is not started before assigning the value to it (P0593R6). To fix the problem, placement new should be used:

int *p = (int*)malloc(sizeof(int));
new (p) int;
*p = 10;

Do we really have to call a default constructor that is trivial to start the lifetime of the object?

At the same time, the code does not have undefined behavior in pure C. But, what if I allocate an int in C code and use it in C++ code?

// C source code:
int *alloc_int(void)
{
    int *p = (int*)malloc(sizeof(int));
    *p = 10;
    return p;
}

// C++ source code:
extern "C" int *alloc_int(void);

auto p = alloc_int();
*p = 20;

Is it still undefined behavior?

7
  • 8
    For int? No. For std::string? Yes. Commented Aug 12, 2020 at 14:50
  • 8
    @Eljay For int, also yes. It's just that it won't cause problems in practice if you don't do it. For std::string, it just will obviously cause problems. Commented Aug 12, 2020 at 14:56
  • Pre C++20 you can add a placement new. It would then be well formed and it probably wouldn't cost anything. Commented Aug 12, 2020 at 14:56
  • 9
    What are the new rules in C++20 that change this? Commented Aug 12, 2020 at 15:00
  • 4
    Shouldn't it be int *p = (int*)malloc(sizeof(int)); p = new(p) int;? I once realized that not assigning the result of placement new may cause fatal effects as well (although it might look a bit silly). Commented Aug 12, 2020 at 15:04

2 Answers 2

71

Is it true?

Yes. Technically speaking, no part of:

int *p = (int*)malloc(sizeof(int));

actually creates an object of type int, so dereferencing p is UB since there is no actual int there.

Do we really have to call default constructor that is trivial to start the life time of the object?

Do you have to per the C++ object model to avoid undefined behavior pre-C++20? Yes. Will any compiler actually cause harm by you not doing this? Not that I'm aware of.

[...] Is it still undefined behavior?

Yes. Pre-C++20, you still didn't actually create an int object anywhere so this is UB.

Sign up to request clarification or add additional context in comments.

15 Comments

Comments are not for extended discussion; this conversation has been moved to chat.
Why isn't the language in timsong-cpp.github.io/cppwp/n3337/basic.life#1.1 sufficient for it not to be UB? After all, storage of proper size and alignment was obtained for int in the example -- the lifetime of the int object begins there.
@avakar because [intro.object] is an exhaustive listing of how objects are created, and "storage was allocated" isn't one of them (until C++20)
@Caleth: That inconsistency means there was a defect in the Standard. It did not make [basic.life] non-authoritative. (Furthermore that "exhaustive list" in [intro.object] said "An object is created by a definition, by a new-expression or by the implementation when needed" which is sufficiently catch-all to include the needs of [basic.life])
@Caleth: Which of the exhaustive list in [intro.object] created the array of unsigned char which is referred to in eel.is/c++draft/basic.types#general-4 ? Clearly [intro.object] is not and never has been exhaustive.
|
45

Yes, it was UB. The list of ways an int can exist was enumerated, and none applies there, unless you hold that malloc is acausal.

It was widely considered a flaw in the standard, but one of low importance, because the optimizations done by C++ compilers around that particular bit of UB didn't cause problems with that use case.

As for the 2nd question, C++ does not mandate how C++ and C interact. So all interaction with C is ... UB, aka behaviour undefined by the C++ standard.

14 Comments

Can you expand on the enumerated list of ways for an int to exist? I remember asking a similar question about the lifespan of primitive types, and being told that a primitive could "exist" simply by saying it exists because the spec didn't say otherwise. It sounds like I may have missed out on a useful section of the spec! I'd love to know what section I should have perused!
@CortAmmon The enumerated list of ways for an object (of any type) to exist in C++20 are in [intro.object]: (1) by definition (2) by new-expression (3) implicitly per the new rules in P0593 (4) changing the active member of a union (5) temporary. (3) is new in C++20, (4) was new in C++17.
Is C/C++ interaction really UB? It would make more sense to be implementation-defined, rather than undefined, otherwise it'd be strange to even have the extern "C" syntax at all.
@Ruslan: Implementations are free to define any behaviour ISO C++ leaves undefined. (For example gcc -fno-strict-aliasing, or MSVC by default). Saying "implementation defined" would require all C++ implementations to define some way in which they interoperate with some C implementation, so it makes sense to leave fully up to implementation whether they want to do anything like that or not.
@PeterCordes: I wonder why so many people fail to recognize that distinction between IDB and UB, and adopt some fanciful notion that the Standard's failure to mandate that all implementations process a construct meaningfully implies a judgment that no implementations should be expected to do so, and implementations which don't do so must not as a consequence be viewed as inferior.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.