QVariant: Support emplace

[ChangeLog][QtCore][QVariant] Implemented in-place construction for
QVariant. The constructor taking std::in_place_type<Type> constructs
an object of type Type directly inside QVariant's storage, without any
further copy or move operations. QVariant::emplace() does the same
when replacing the content of an existing QVariant and tries to reuse
previously-allocated memory.

Fixes: QTBUG-112187
Change-Id: I16614ad701fa3bb583976ed2001bb312f119a51f
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
Reviewed-by: Ivan Solovev <ivan.solovev@qt.io>
This commit is contained in:
Fabian Kosmale 2023-03-23 10:12:10 +01:00
parent 18a2c62c07
commit f564e905c1
3 changed files with 200 additions and 0 deletions

View File

@ -545,6 +545,28 @@ QVariant::QVariant(const QVariant &p)
non-initializer list \c{in_place_type_t} overload.
*/
/*!
\fn template <typename Type, typename... Args, if_constructible<Type, Args...> = true> QVariant::emplace(Args&&... args)
\since 6.6
Replaces the object currently held in \c{*this} with an object of
type \c{Type}, constructed from \a{args}\c{...}. If \c{*this} was non-null,
the previously held object is destroyed first.
If possible, this method will reuse memory allocated by the QVariant.
Returns a reference to the newly-created object.
*/
/*!
\fn template <typename Type, typename List, typename... Args, if_constructible<Type, std::initializer_list<List> &, Args...> = true> QVariant::emplace(std::initializer_list<List> list, Args&&... args)
\since 6.6
\overload
This overload exists to support types with constructors taking an
\c initializer_list. It behaves otherwise equivalent to the
non-initializer list overload.
*/
QVariant::QVariant(std::in_place_t, QMetaType type) : d(type.iface())
{
// we query the metatype instead of detecting it at compile time
@ -555,6 +577,47 @@ QVariant::QVariant(std::in_place_t, QMetaType type) : d(type.iface())
}
}
/*!
\internal
Returns a pointer to data suitable for placement new
of an object of type \a type
Changes the variant's metatype to \a type
*/
void *QVariant::prepareForEmplace(QMetaType type)
{
/* There are two cases where we can reuse the existing storage
(1) The new type fits in QVariant's SBO storage
(2) We are using the externally allocated storage, the variant is
detached, and the new type fits into the existing storage.
In all other cases (3), we cannot reuse the storage.
*/
auto typeFits = [&] {
auto newIface = type.iface();
auto oldIface = d.typeInterface();
auto newSize = PrivateShared::computeAllocationSize(newIface->size, newIface->alignment);
auto oldSize = PrivateShared::computeAllocationSize(oldIface->size, oldIface->alignment);
return newSize <= oldSize;
};
if (Private::canUseInternalSpace(type.iface())) { // (1)
clear();
d.packedType = quintptr(type.iface()) >> 2;
return d.data.data;
} else if (d.is_shared && isDetached() && typeFits()) { // (2)
QtMetaTypePrivate::destruct(d.typeInterface(), d.data.shared->data());
// compare QVariant::PrivateShared::create
const auto ps = d.data.shared;
const auto align = type.alignOf();
ps->offset = PrivateShared::computeOffset(ps, align);
d.packedType = quintptr(type.iface()) >> 2;
return ps->data();
}
// (3)
QVariant newVariant(std::in_place, type);
swap(newVariant);
// const cast is safe, we're in a non-const method
return const_cast<void *>(d.storage());
}
/*!
\fn QVariant::QVariant(const QString &val) noexcept

View File

@ -444,6 +444,43 @@ public:
{ return d.storage(); }
inline const void *data() const { return constData(); }
private:
template <typename Type>
void verifySuitableForEmplace()
{
static_assert(!std::is_reference_v<Type>,
"QVariant does not support reference types");
static_assert(!std::is_const_v<Type>,
"QVariant does not support const types");
static_assert(std::is_copy_constructible_v<Type>,
"QVariant requires that the type is copyable");
static_assert(std::is_destructible_v<Type>,
"QVariant requires that the type is destructible");
}
template <typename Type, typename... Args>
Type &emplaceImpl(Args&&... args)
{
verifySuitableForEmplace<Type>();
auto data = static_cast<Type *>(prepareForEmplace(QMetaType::fromType<Type>()));
return *q20::construct_at(data, std::forward<Args>(args)...);
}
public:
template <typename Type, typename... Args,
if_constructible<Type, Args...> = true>
Type &emplace(Args&&... args)
{
return emplaceImpl<Type>(std::forward<Args>(args)...);
}
template <typename Type, typename List, typename... Args,
if_constructible<Type, std::initializer_list<List> &, Args...> = true>
Type &emplace(std::initializer_list<List> list, Args&&... args)
{
return emplaceImpl<Type>(list, std::forward<Args>(args)...);
}
template<typename T, typename = std::enable_if_t<!std::is_same_v<std::decay_t<T>, QVariant>>>
void setValue(T &&avalue)
{
@ -580,6 +617,8 @@ private:
// used to setup the QVariant internals for the "real" inplace ctor
QVariant(std::in_place_t, QMetaType type);
// helper for emplace
void *prepareForEmplace(QMetaType type);
// These constructors don't create QVariants of the type associated
// with the enum, as expected, but they would create a QVariant of

View File

@ -380,6 +380,7 @@ private slots:
void copyNonDefaultConstructible();
void inplaceConstruct();
void emplace();
void getIf_int() { getIf_impl(42); }
void getIf_QString() { getIf_impl(u"string"_s); };
@ -5767,6 +5768,103 @@ void tst_QVariant::inplaceConstruct()
}
}
struct LargerThanInternalQVariantStorage {
char data[6 * sizeof(void *)];
};
struct alignas(256) LargerThanInternalQVariantStorageOveraligned {
char data[6 * sizeof(void *)];
};
struct alignas(128) SmallerAlignmentEvenLargerSize {
char data[17 * sizeof(void *)];
};
void tst_QVariant::emplace()
{
{
// can emplace non default constructible + can emplace on null variant
NonDefaultConstructible ndc(42);
QVariant var;
var.emplace<NonDefaultConstructible>(42);
QVERIFY(get_if<NonDefaultConstructible>(&var));
QCOMPARE(get<NonDefaultConstructible>(var), ndc);
}
{
// can emplace using ctor taking initializer_list
QVariant var;
var.emplace<std::vector<int>>({0, 1, 2, 3, 4});
auto vecPtr = get_if<std::vector<int>>(&var);
QVERIFY(vecPtr);
QCOMPARE(vecPtr->size(), 5U);
for (int i = 0; i < 5; ++i)
QCOMPARE(vecPtr->at(size_t(i)), i);
}
// prequisites for the test
QCOMPARE_LE(sizeof(std::vector<int>), sizeof(std::string));
QCOMPARE(alignof(std::vector<int>), alignof(std::string));
{
// emplace can reuse storage
auto var = QVariant::fromValue(std::string{});
QVERIFY(var.data_ptr().is_shared);
auto data = var.constData();
std::vector<int> &vec = var.emplace<std::vector<int>>(3, 42);
/* alignment is the same, so the pointer is exactly the same;
no offset change */
auto expected = std::vector<int>{42, 42, 42};
QCOMPARE(get_if<std::vector<int>>(&var), &vec);
QCOMPARE(get<std::vector<int>>(var), expected);
QCOMPARE(var.constData(), data);
}
{
// emplace can't reuse storage if the variant is shared
auto var = QVariant::fromValue(std::string{});
[[maybe_unused]] QVariant causesSharing = var;
QVERIFY(var.data_ptr().is_shared);
auto data = var.constData();
var.emplace<std::vector<int>>(3, 42);
auto expected = std::vector<int>{42, 42, 42};
QVERIFY(get_if<std::vector<int>>(&var));
QCOMPARE(get<std::vector<int>>(var), expected);
QCOMPARE_NE(var.constData(), data);
}
{
// emplace puts element into the correct place - non-shared
QVERIFY(QVariant::Private::canUseInternalSpace(QMetaType::fromType<QString>().iface()));
QVariant var;
var.emplace<QString>(QChar('x'));
QVERIFY(!var.data_ptr().is_shared);
}
{
// emplace puts element into the correct place - shared
QVERIFY(!QVariant::Private::canUseInternalSpace(QMetaType::fromType<std::string>().iface()));
QVariant var;
var.emplace<std::string>(42, 'x');
QVERIFY(var.data_ptr().is_shared);
}
{
// emplace does not reuse the storage if alignment is too large
auto iface = QMetaType::fromType<LargerThanInternalQVariantStorage>().iface();
QVERIFY(!QVariant::Private::canUseInternalSpace(iface));
auto var = QVariant::fromValue(LargerThanInternalQVariantStorage{});
auto data = var.constData();
var.emplace<LargerThanInternalQVariantStorageOveraligned>();
QCOMPARE_NE(var.constData(), data);
}
{
// emplace does reuse the storage if new alignment and size are together small enough
auto iface = QMetaType::fromType<LargerThanInternalQVariantStorageOveraligned>().iface();
QVERIFY(!QVariant::Private::canUseInternalSpace(iface));
auto var = QVariant::fromValue(LargerThanInternalQVariantStorageOveraligned{});
auto data = var.constData();
var.emplace<SmallerAlignmentEvenLargerSize>();
// no exact match below - the alignment is after all different
QCOMPARE_LE(quintptr(var.constData()), quintptr(data));
QCOMPARE_LE(quintptr(var.constData()),
quintptr(data) + sizeof(LargerThanInternalQVariantStorageOveraligned));
}
}
void tst_QVariant::getIf_NonDefaultConstructible()
{
getIf_impl(NonDefaultConstructible{42});