Library: Uninitialised-Storage array (see #1204)

Introduced as remedy for a long standing sloppiness:
Using a `char[]` together with `reinterpret_cast` in storage management helpers
bears danger of placing objects with wrong alignment; moreover, there are increasing
risks that modern code optimisers miss the ''backdoor access'' and might apply too
aggressive rewritings.

With C++17, there is a standard conformant way to express such a usage scheme.
 * `lib::UninitialisedStorage` can now be used in a situation (e.g. as in `ExtentFamily`)
   where a complete block of storage is allocated once and then subsequently used
   to plant objects one by one
 * moreover, I went over the code base and adapted the most relevant usages of
   ''placement-new into buffer'' to also include the `std::launder()` marker
This commit is contained in:
Fischlurch 2023-12-02 23:56:46 +01:00
parent 6707962bca
commit c5679b0fd0
16 changed files with 247 additions and 48 deletions

View file

@ -57,6 +57,7 @@
#include "lib/error.hpp"
#include <cstddef>
#include <utility>
#include <list>
@ -81,7 +82,8 @@ namespace lib {
{
struct Allocation
{
char buf_[sizeof(TY)];
alignas(TY)
std::byte buf_[sizeof(TY)];
template<typename...ARGS>
TY&
@ -93,7 +95,7 @@ namespace lib {
TY&
access()
{
return reinterpret_cast<TY&> (buf_);
return * std::launder (reinterpret_cast<TY*> (&buf_));
}
void
discard() /// @warning strong assumption made here: Payload was created

View file

@ -209,9 +209,6 @@ namespace diff{
{
scopes_.emplace();
TreeMutator::Handle placementHandle (scopes_.top());
static_assert (buffSiz >= sizeof(typename MutatorStack::value_type)
,"insufficient working buffer for TreeMutator");
return placementHandle;
}

View file

@ -54,6 +54,8 @@
#include <boost/static_assert.hpp>
#include <boost/format.hpp>
#include <iostream>
#include <cstddef>
#include <new>
@ -67,14 +69,14 @@ namespace util {
namespace { // implementation details...
inline boost::format&
accessImpl (char* buffer)
accessImpl (std::byte* buffer)
{
return reinterpret_cast<boost::format&> (*buffer);
return * std::launder (reinterpret_cast<boost::format*> (buffer));
}
inline void
destroyImpl (char* buffer)
destroyImpl (std::byte* buffer)
{
accessImpl(buffer).~format();
}
@ -83,7 +85,7 @@ namespace util {
/** in case the formatting of a (primitive) value fails,
* we try to supply an error indicator instead */
void
pushFailsafeReplacement (char* formatter, const char* errorMsg =NULL)
pushFailsafeReplacement (std::byte* formatter, const char* errorMsg =NULL)
try {
string placeholder("<Error");
if (errorMsg){
@ -98,7 +100,7 @@ namespace util {
inline void
suppressInsufficientArgumentErrors (char* formatter)
suppressInsufficientArgumentErrors (std::byte* formatter)
{
using namespace boost::io;
accessImpl(formatter).exceptions (all_error_bits ^ too_few_args_bit);

View file

@ -159,11 +159,11 @@ namespace util {
/** size of an opaque implementation Buffer */
enum{ FORMATTER_SIZE = lib::meta::SizeTrait::BOOST_FORMAT };
typedef char Implementation[FORMATTER_SIZE];
typedef std::byte Implementation[FORMATTER_SIZE];
/** @internal buffer to hold a boost::format */
mutable Implementation formatter_;
alignas(size_t) mutable Implementation formatter_;
/** call into the opaque implementation */

View file

@ -106,6 +106,7 @@
#include "lib/error.hpp"
#include "lib/uninitialised-storage.hpp"
#include "lib/meta/duck-detector.hpp"
#include "lib/meta/function.hpp"
#include "lib/meta/trait.hpp"
@ -867,13 +868,11 @@ namespace lib {
using Group = std::array<RES, grp>;
using Iter = typename Group::iterator;
struct Buffer
: lib::UninitialisedStorage<RES,grp>
{
char storage[sizeof(Group)];
Group& group() { return reinterpret_cast<Group&> (storage); }
Iter begin() { return group().begin();}
Iter end() { return group().end(); }
Group& group(){ return *this; }
Iter begin() { return group().begin();}
Iter end() { return group().end(); }
};
Buffer buff_;
uint pos_{0};

View file

@ -90,6 +90,25 @@ namespace meta {
};
/**
* Metafunction " max( alignof(T) ) for T in TYPES "
*/
template<class TYPES>
struct maxAlign;
template<>
struct maxAlign<NullType>
{
static constexpr int value = 0;
};
template<class TY, class TYPES>
struct maxAlign<Node<TY,TYPES>>
{
static constexpr size_t thisval = alignof(TY);
static constexpr size_t nextval = maxAlign<TYPES>::value;
static constexpr size_t value = nextval > thisval? nextval:thisval;
};
/**
* Metafunction to check if a specific type is contained
* in a given typelist. Only exact match is detected.

View file

@ -86,8 +86,9 @@
#include <boost/lexical_cast.hpp>
#include <type_traits>
#include <cstddef>
#include <utility>
#include <type_traits>
namespace lib {
@ -213,7 +214,8 @@ namespace lib {
/** Inner capsule managing the contained object (interface) */
struct Buffer
{
char content_[siz];
alignas(size_t) std::byte content_[siz];
void* ptr() { return &content_; }
virtual ~Buffer() {} ///< this is an ABC with VTable
@ -255,7 +257,7 @@ namespace lib {
SUB&
get() const ///< core operation: target is contained within the inline buffer
{
return *reinterpret_cast<SUB*> (unConst(this)->ptr());
return * std::launder (reinterpret_cast<SUB*> (unConst(this)->ptr()));
}
~Buff()
@ -328,12 +330,12 @@ namespace lib {
Buffer&
buff()
{
return *reinterpret_cast<Buffer*> (&storage_);
return * std::launder (reinterpret_cast<Buffer*> (&storage_));
}
const Buffer&
buff() const
{
return *reinterpret_cast<const Buffer *> (&storage_);
return * std::launder (reinterpret_cast<const Buffer *> (&storage_));
}
@ -614,13 +616,14 @@ namespace lib {
: util::NonCopyable
{
mutable char buf_[siz];
alignas(BA) mutable
std::byte buf_[siz];
BA&
getObj() const
{
return reinterpret_cast<BA&> (buf_);
return * std::launder (reinterpret_cast<BA*> (&buf_));
}
void

View file

@ -166,6 +166,9 @@
#include "lib/meta/duck-detector.hpp"
#include "lib/util.hpp"
#include <cstddef>
#include <new>
namespace lib {
@ -383,12 +386,13 @@ namespace lib {
/* === embedded object in buffer === */
/** Storage for embedded objects */
mutable char buf_[siz];
alignas(IFA) mutable
std::byte buf_[siz];
IFA&
accessEmbedded() const
{
return reinterpret_cast<IFA&> (buf_);
return * std::launder (reinterpret_cast<IFA*> (&buf_));
}
void

View file

@ -65,6 +65,7 @@
#include <type_traits>
#include <utility>
#include <cstddef>
@ -86,7 +87,8 @@ namespace wrapper {
template<typename X, typename COND =void>
class ReplaceableItem
{
char content_[sizeof(X)];
alignas(X)
std::byte content_[sizeof(X)];
public:
ReplaceableItem()
@ -131,7 +133,7 @@ namespace wrapper {
X&
access()
{
return reinterpret_cast<X&>(content_);
return * std::launder (reinterpret_cast<X*> (&content_));
}
void

View file

@ -75,6 +75,7 @@
#include "lib/meta/trait.hpp"
#include "lib/iter-adapter.hpp"
#include <cstddef>
#include <type_traits>
@ -113,15 +114,15 @@ namespace lib {
class ElementHolder
: util::NonCopyable
{
mutable char buf_[siz];
alignas(I) mutable
std::byte buf_[siz];
public:
I&
accessObj() const
{
return reinterpret_cast<I&> (buf_);
return * std::launder (reinterpret_cast<I*> (&buf_));
}
void

View file

@ -0,0 +1,130 @@
/*
UNINITIALISED-STORAGE.hpp - array-like container with raw storage
Copyright (C) Lumiera.org
2023, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file uninitialised-storage.hpp
** A raw memory block with proper alignment and array access.
** This is a building block for custom containers and memory management schemes.
** Regular containers in C++ always ensure invocation of any object's constructors
** and destructors -- which is a boon and prevents a lot of consistency problems.
** When constructing custom management schemes however, this automatic initialisation
** can be problematic; some objects require constructor arguments, preventing mass
** initialisation; and initialising a large memory block has considerable cost,
** which may be wasted it the intended usage is to plant objects into that space
** later, on demand. The std::vector::reserve(size) function can often be used
** to circumvent those problems — yet unfortunately std::vector has the hard
** requirement on the content objects to be at least _movable_ to support
** automatic storage growth.
**
** A traditional workaround is to exploit a programming trick relying on a
** _forced cast:_ Storage is declared as `char[]` without initialiser; since
** `char` is a primitive type, C++ will not default-initialise the storage then
** for sake of plain-old-C compatibility. A special accessor function will then
** force-cast into the desired target type. However well established, this
** practice is riddled with various problems
** - it requires very precise setup and any mistake leads to memory corruption
** - according to the C++ standard, such an access involves _undefined behaviour_
** - compilers are getting better and increasingly aggressive with performing global
** optimisation and may miss the secondary hidden access under some circumstances.
** - when objects with `const` data fields are placed into such a buffer, the compiler
** may perform constant folding based on the current/initial object values and thus
** fail to propagate changes due to other object instances being placed into storage.
**
** Attempts were made to codify this kind of usage properly into the standard; these
** turned out to be problematic however and were [deprecated again]. Starting with
** C++17, fortunately there is now a way to express this kind of raw unprotected
** access through standard conformant ways and marked clearly to prevent overly
** zealous optimisation. Since the typical use is for allocating a series of
** storage slots with a well defined target type, this implementation
** relies on std::array as »front-end« for access.
** - target type `T` and size are given as template parameters
** - the storage is defined as `std::byte` to express its very purpose
** - the secondary access is marked by `std::launder` to prevent optimisation
** - an implicit conversion path to the corresponding array type is provided
** - subscript operators enable direct access to the raw storage
** - helper functions allow for placement new and delete.
** [deprecated again]: https://stackoverflow.com/a/71828512
** @see extent-family.hpp
** @see vault::gear::TestChainLoad::Rule where this setup matters
*/
#ifndef LIB_UNINITIALISED_STORAGE_H
#define LIB_UNINITIALISED_STORAGE_H
#include <cstddef>
#include <utility>
#include <array>
#include <new>
namespace lib {
/**
* Block of raw uninitialised storage with array like access.
* @tparam T the nominal type assumed to sit in each »slot«
* @tparam cnt number of »slots« in the array
*/
template<typename T, size_t cnt>
class UninitialisedStorage
{
using _Arr = std::array<T,cnt>;
alignas(T) std::byte buffer_[sizeof(_Arr)];
public:
_Arr&
array()
{
return * std::launder (reinterpret_cast<_Arr* > (&buffer_));
}
_Arr const&
array() const
{
return * std::launder (reinterpret_cast<_Arr const*> (&buffer_));
}
operator _Arr&() { return array(); }
operator _Arr const&() const { return array(); }
T & operator[] (size_t idx) { return array()[idx]; }
T const& operator[] (size_t idx) const { return array()[idx]; }
template<typename...Args>
T&
createAt (size_t idx, Args&& ...args)
{
return *new(&operator[](idx)) T{std::forward<Args>(args)...};
}
void
destroyAt (size_t idx)
{
operator[](idx).~T();
}
};
} // namespace lib
#endif /*LIB_UNINITIALISED_STORAGE_H*/

View file

@ -88,6 +88,7 @@
#include "lib/util.hpp"
#include <type_traits>
#include <cstddef>
#include <utility>
#include <string>
@ -222,10 +223,11 @@ namespace lib {
template<typename TYPES>
class Variant
{
// WARNING: never add any member field before the storage_ array /////////////////////////TICKET #1204
public:
enum { SIZ = meta::maxSize<typename TYPES::List>::value };
enum { SIZ = meta::maxSize<typename TYPES::List>::value
, ALIGN = meta::maxAlign<typename TYPES::List>::value
};
template<typename RET>
using VisitorFunc = typename variant::VFunc<RET>::template VisitorInterface<TYPES>;
@ -264,7 +266,8 @@ namespace lib {
struct Buffer
: meta::VirtualCopySupportInterface<Buffer>
{
char content_[SIZ];
alignas(ALIGN)
std::byte content_[SIZ];
void* ptr() { return &content_; }
@ -287,7 +290,7 @@ namespace lib {
TY&
access() const ///< core operation: target is contained within the inline buffer
{
return *reinterpret_cast<TY*> (unConst(this)->ptr());
return * std::launder (reinterpret_cast<TY*> (unConst(this)->ptr()));
}
~Buff()
@ -396,12 +399,12 @@ namespace lib {
Buffer&
buffer()
{
return *reinterpret_cast<Buffer*> (&storage_);
return * std::launder (reinterpret_cast<Buffer*> (&storage_));
}
Buffer const&
buffer() const
{
return *reinterpret_cast<const Buffer*> (&storage_);
return * std::launder (reinterpret_cast<const Buffer*> (&storage_));
}
template<typename X>

View file

@ -49,6 +49,7 @@
#include "lib/util.hpp"
#include <functional>
#include <cstddef>
namespace lib {
@ -113,8 +114,9 @@ namespace wrapper {
using TY_unconst = std::remove_const_t<TY>;
mutable
char content_[sizeof(TY)];
alignas(TY) mutable
std::byte content_[sizeof(TY)];
bool created_;
template<typename REF>
@ -135,7 +137,7 @@ namespace wrapper {
TY&
access () const
{
return reinterpret_cast<TY&> (content_);
return * std::launder (reinterpret_cast<TY*> (&content_));
}
TY_unconst&

View file

@ -175,7 +175,7 @@ namespace engine {
throw error::Invalid ("Buffer with the given ID not yet emitted");
diagn::Block& memoryBlock = access_emitted (bufferID);
TY* converted = reinterpret_cast<TY*> (memoryBlock.accessMemory());
TY* converted = std::launder (reinterpret_cast<TY*> (memoryBlock.accessMemory()));
REQUIRE (converted);
return *converted;

View file

@ -42,6 +42,7 @@
#include "vault/common.hpp"
#include "lib/uninitialised-storage.hpp"
#include "lib/iter-adapter.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"
@ -90,16 +91,18 @@ namespace mem {
private:
using _UniqueStoragePtr = std::unique_ptr<lib::UninitialisedStorage<T,siz>>;
/** Entry in the Extents management datastructure */
struct Storage
: std::unique_ptr<char[]>
: _UniqueStoragePtr
{
/**
* @note default ctor immediately allocates the full storage,
* but without initialisation since payload is `char`
* but without any initialisation of memory content
*/
Storage()
: unique_ptr{new char[sizeof(Extent)]}
: _UniqueStoragePtr{new lib::UninitialisedStorage<T,siz>}
{ }
/** access projected Extent storage type
@ -108,8 +111,9 @@ namespace mem {
Extent&
access()
{
ENSURE (get() != nullptr);
return reinterpret_cast<Extent&> (*get());
auto* rawStorage = this->get();
ENSURE (rawStorage != nullptr);
return static_cast<Extent&> (rawStorage->array());
}
};
using Extents = std::vector<Storage>;

View file

@ -110692,7 +110692,7 @@ class Something
<node CREATED="1570817565257" ID="ID_872374381" MODIFIED="1570817571504" TEXT="auf x86 harmlos"/>
<node CREATED="1570817572097" ID="ID_942342414" MODIFIED="1570817577917" TEXT="aber nicht portabel"/>
</node>
<node CREATED="1570812031118" FOLDED="true" ID="ID_1018321072" MODIFIED="1570812037369" TEXT="Alignment bei placement-new">
<node CREATED="1570812031118" FOLDED="true" ID="ID_1018321072" MODIFIED="1701555783331" TEXT="Alignment bei placement-new">
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1570812038974" ID="ID_716754107" MODIFIED="1570812068335" TEXT="der Buffer mu&#xdf; passendes Alignment f&#xfc;r das Target aufweisen">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1570812070369" ID="ID_490799733" MODIFIED="1570812089910" TEXT="der Compiler kann das (meist/immer) nicht verifizieren">
@ -110750,6 +110750,37 @@ class Something
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1701555943105" ID="ID_55328417" MODIFIED="1701559664023" TEXT="char[] durch aligned raw storage abl&#xf6;sen">
<icon BUILTIN="bell"/>
<node CREATED="1701555969695" ID="ID_1535597438" MODIFIED="1701556221036" TEXT="&#x201e;char is calling for trouble&#x201c;">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
Da alignof(char) &#8801; 1, ist es gradezu eine &#187;Steilvorlage&#171; f&#252;r Probleme, wenn man einen Allocation-Buffer per char[] deklariert. GCC macht das nicht (er f&#228;ngt Allokationen immer an 64bit-Grenzen an), aber grunds&#228;tzlich d&#252;rfte ein Compiler ein char[] anfangen, wo er grad' lustig ist. Besonders gef&#228;hrlich, wenn das Array <i>in ein anderes Objekt eingebettet</i>&#160;wird. Nur den zuletzt genannten Fall habe ich 2019 abgekl&#228;rt; gegen alle sonstigen <i>Schnaps-Ideen </i>gibt es keinen Schutz &#8212; es sei denn, man h&#228;lt sich an die Sprachmittel
</p>
</body>
</html>
</richcontent>
</node>
<node CREATED="1701556228394" ID="ID_1444144444" LINK="https://en.cppreference.com/w/cpp/types/aligned_storage" MODIFIED="1701559705550" TEXT="es g&#xe4;be daf&#xfc;r ein Sprachmittel: std::aligned_storage">
<node CREATED="1701556795662" ID="ID_1263844060" LINK="https://stackoverflow.com/a/71828512" MODIFIED="1701556817727" TEXT="wenngleich auch deprecated f&#xfc;r C++23"/>
<node CREATED="1701559709278" ID="ID_78471984" MODIFIED="1701559737569" TEXT="direkt durch ein std::byte[] ersetzen mit alignas(T)">
<icon BUILTIN="idea"/>
</node>
</node>
<node COLOR="#338800" CREATED="1701556464842" ID="ID_1873202439" MODIFIED="1701559743611" TEXT="versuchsweise: vault::mem::ExtentFamily umgestellt">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1701559745027" ID="ID_1331044905" MODIFIED="1701574720900" TEXT="lib::UninitialisedStorage bereitstellen">
<icon BUILTIN="button_ok"/>
<node COLOR="#338800" CREATED="1701561920506" ID="ID_889775258" MODIFIED="1701561928182" TEXT="&#x27f6; und f&#xfc;r ExtentFamilty verwenden">
<icon BUILTIN="button_ok"/>
</node>
</node>
</node>
</node>
</node>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1690740456817" FOLDED="true" ID="ID_1080126518" MODIFIED="1690740564054" TEXT="#853 Portabilit&#xe4;t">