Is it OK to discard placement new return value when initializing objects

后端 未结 1 1091
醉梦人生
醉梦人生 2020-12-20 16:31

This question originates from the comment section in this thread, and has also got an answer there. However, I think it is too important to be left in the comment sectio

相关标签:
1条回答
  • 2020-12-20 16:54

    Ignoring the return value is not OK both pedantically and practically.

    From a pedantic point of view

    For p = new(p) T{...}, p qualifies as a pointer to an object created by a new-expression, which does not hold for new(p) T{...}, despite the fact that the value is the same. In the latter case, it only qualifies as pointer to an allocated storage.

    The non-allocating global allocation function returns its argument with no side effect implied, but a new-expression (placement or not) always returns a pointer to the object it creates, even if it happens to use that allocation function.

    Per cppref's description about the delete-expression (emphasis mine):

    For the first (non-array) form, expression must be a pointer to a object type or a class type contextually implicitly convertible to such pointer, and its value must be either null or pointer to a non-array object created by a new-expression, or a pointer to a base subobject of a non-array object created by a new-expression. If expression is anything else, including if it is a pointer obtained by the array form of new-expression, the behavior is undefined.

    Failing to p = new(p) T{...} therefore makes delete p undefined behavior.

    From a practical point of view

    Technically, without p = new(p) T{...}, p does not point to the newly-initialized T, despite the fact that the value (memory address) is the same. The compiler may therefore assume that p still refers to the T that was there before the placement new. Consider the code

    p = new(p) T{...} // (1)
    ...
    new(p) T{...} // (2)
    

    Even after (2), the compiler may assume that p still refers to the old value initialized at (1), and make incorrect optimizations thereby. For example, if T had a const member, the compiler might cache its value at (1) and still use it even after (2).

    p = new(p) T{...} effectively prohibits this assumption. Another way is to use std::launder(), but it is easier and cleaner to just assign the return value of placement new back to p.

    Something you may do to avoid the pitfall

    template <typename T, typename... Us>
    void init(T*& p, Us&&... us) {
      p = new(p) T(std::forward<Us>(us)...);
    }
    
    template <typename T, typename... Us>
    void list_init(T*& p, Us&&... us) {
      p = new(p) T{std::forward<Us>(us)...};
    }
    

    These function templates always set the pointer internally. With std::is_aggregate available since C++17, the solution can be improved by automatically choosing between () and {} syntax based on whether T is an aggregate type.

    0 讨论(0)
提交回复
热议问题