Move qAsConst() and qExchange() to QtTypeTraits

It's the least worst place we could think of.

Add the qttypetraits.h include to qforeach, because otherwise
the tests fail to build.

Task-number: QTBUG-106154
Task-number: QTBUG-99313
Change-Id: I841f22e887f351146589377ec2376957e2adfd5e
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Ivan Solovev 2022-09-30 13:12:42 +02:00
parent 394e9a8d06
commit dff985140a
5 changed files with 116 additions and 114 deletions

View File

@ -7,6 +7,7 @@
#include <QtCore/qglobal.h> #include <QtCore/qglobal.h>
#include <QtCore/qtdeprecationmarkers.h> #include <QtCore/qtdeprecationmarkers.h>
#include <QtCore/qttypetraits.h>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE

View File

@ -227,102 +227,6 @@ void qAbort()
// localtime() -- but not localtime_r(), which we use when threaded // localtime() -- but not localtime_r(), which we use when threaded
// strftime() -- not used (except in tests) // strftime() -- not used (except in tests)
/*!
\fn template <typename T> typename std::add_const<T>::type &qAsConst(T &t)
\relates <QtGlobal>
\since 5.7
Returns \a t cast to \c{const T}.
This function is a Qt implementation of C++17's std::as_const(),
a cast function like std::move(). But while std::move() turns
lvalues into rvalues, this function turns non-const lvalues into
const lvalues. Like std::as_const(), it doesn't work on rvalues,
because it cannot be efficiently implemented for rvalues without
leaving dangling references.
Its main use in Qt is to prevent implicitly-shared Qt containers
from detaching:
\snippet code/src_corelib_global_qglobal.cpp as-const-0
Of course, in this case, you could (and probably should) have declared
\c s as \c const in the first place:
\snippet code/src_corelib_global_qglobal.cpp as-const-1
but often that is not easily possible.
It is important to note that qAsConst() does not copy its argument,
it just performs a \c{const_cast<const T&>(t)}. This is also the reason
why it is designed to fail for rvalues: The returned reference would go
stale too soon. So while this works (but detaches the returned object):
\snippet code/src_corelib_global_qglobal.cpp as-const-2
this would not:
\snippet code/src_corelib_global_qglobal.cpp as-const-3
To prevent this construct from compiling (and failing at runtime), qAsConst() has
a second, deleted, overload which binds to rvalues.
*/
/*!
\fn template <typename T> void qAsConst(const T &&t)
\relates <QtGlobal>
\since 5.7
\overload
This overload is deleted to prevent a dangling reference in code like
\snippet code/src_corelib_global_qglobal.cpp as-const-4
*/
/*!
\fn template <typename T, typename U = T> T qExchange(T &obj, U &&newValue)
\relates <QtGlobal>
\since 5.14
Replaces the value of \a obj with \a newValue and returns the old value of \a obj.
This is Qt's implementation of std::exchange(). It differs from std::exchange()
only in that it is \c constexpr already in C++14, and available on all supported
compilers.
Here is how to use qExchange() to implement move constructors:
\code
MyClass(MyClass &&other)
: m_pointer{qExchange(other.m_pointer, nullptr)},
m_int{qExchange(other.m_int, 0)},
m_vector{std::move(other.m_vector)},
...
\endcode
For members of class type, we can use std::move(), as their move-constructor will
do the right thing. But for scalar types such as raw pointers or integer type, move
is the same as copy, which, particularly for pointers, is not what we expect. So, we
cannot use std::move() for such types, but we can use std::exchange()/qExchange() to
make sure the source object's member is already reset by the time we get to the
initialization of our next data member, which might come in handy if the constructor
exits with an exception.
Here is how to use qExchange() to write a loop that consumes the collection it
iterates over:
\code
for (auto &e : qExchange(collection, {})
doSomethingWith(e);
\endcode
Which is equivalent to the following, much more verbose code:
\code
{
auto tmp = std::move(collection);
collection = {}; // or collection.clear()
for (auto &e : tmp)
doSomethingWith(e);
} // destroys 'tmp'
\endcode
This is perfectly safe, as the for-loop keeps the result of qExchange() alive for as
long as the loop runs, saving the declaration of a temporary variable. Be aware, though,
that qExchange() returns a non-const object, so Qt containers may detach.
*/
/*! /*!
\macro Q_UNUSED(name) \macro Q_UNUSED(name)
\relates <QtGlobal> \relates <QtGlobal>

View File

@ -54,24 +54,6 @@ QT_BEGIN_NAMESPACE
# define Q_UNIMPLEMENTED() qWarning("Unimplemented code.") # define Q_UNIMPLEMENTED() qWarning("Unimplemented code.")
#endif #endif
// this adds const to non-const objects (like std::as_const)
template <typename T>
constexpr typename std::add_const<T>::type &qAsConst(T &t) noexcept { return t; }
// prevent rvalue arguments:
template <typename T>
void qAsConst(const T &&) = delete;
// like std::exchange
template <typename T, typename U = T>
constexpr T qExchange(T &t, U &&newValue)
noexcept(std::conjunction_v<std::is_nothrow_move_constructible<T>, std::is_nothrow_assignable<T &, U>>)
{
T old = std::move(t);
t = std::forward<U>(newValue);
return old;
}
QT_END_NAMESPACE QT_END_NAMESPACE
// We need to keep QTypeInfo, QSysInfo, QFlags, qDebug & family in qglobal.h for compatibility with Qt 4. // We need to keep QTypeInfo, QSysInfo, QFlags, qDebug & family in qglobal.h for compatibility with Qt 4.

View File

@ -7,6 +7,7 @@
#include <QtCore/qtconfigmacros.h> #include <QtCore/qtconfigmacros.h>
#include <type_traits> #include <type_traits>
#include <utility>
#if 0 #if 0
#pragma qt_class(QtTypeTraits) #pragma qt_class(QtTypeTraits)
@ -22,6 +23,24 @@ constexpr std::underlying_type_t<Enum> qToUnderlying(Enum e) noexcept
return static_cast<std::underlying_type_t<Enum>>(e); return static_cast<std::underlying_type_t<Enum>>(e);
} }
// this adds const to non-const objects (like std::as_const)
template <typename T>
constexpr typename std::add_const<T>::type &qAsConst(T &t) noexcept { return t; }
// prevent rvalue arguments:
template <typename T>
void qAsConst(const T &&) = delete;
// like std::exchange
template <typename T, typename U = T>
constexpr T qExchange(T &t, U &&newValue)
noexcept(std::conjunction_v<std::is_nothrow_move_constructible<T>,
std::is_nothrow_assignable<T &, U>>)
{
T old = std::move(t);
t = std::forward<U>(newValue);
return old;
}
QT_END_NAMESPACE QT_END_NAMESPACE
#endif // QTTYPETRAITS_H #endif // QTTYPETRAITS_H

View File

@ -9,3 +9,99 @@
Converts the enumerator \a e to the equivalent value expressed in its Converts the enumerator \a e to the equivalent value expressed in its
enumeration's underlying type. enumeration's underlying type.
*/ */
/*!
\fn template <typename T> typename std::add_const<T>::type &qAsConst(T &t)
\relates <QtTypeTraits>
\since 5.7
Returns \a t cast to \c{const T}.
This function is a Qt implementation of C++17's std::as_const(),
a cast function like std::move(). But while std::move() turns
lvalues into rvalues, this function turns non-const lvalues into
const lvalues. Like std::as_const(), it doesn't work on rvalues,
because it cannot be efficiently implemented for rvalues without
leaving dangling references.
Its main use in Qt is to prevent implicitly-shared Qt containers
from detaching:
\snippet code/src_corelib_global_qglobal.cpp as-const-0
Of course, in this case, you could (and probably should) have declared
\c s as \c const in the first place:
\snippet code/src_corelib_global_qglobal.cpp as-const-1
but often that is not easily possible.
It is important to note that qAsConst() does not copy its argument,
it just performs a \c{const_cast<const T&>(t)}. This is also the reason
why it is designed to fail for rvalues: The returned reference would go
stale too soon. So while this works (but detaches the returned object):
\snippet code/src_corelib_global_qglobal.cpp as-const-2
this would not:
\snippet code/src_corelib_global_qglobal.cpp as-const-3
To prevent this construct from compiling (and failing at runtime), qAsConst() has
a second, deleted, overload which binds to rvalues.
*/
/*!
\fn template <typename T> void qAsConst(const T &&t)
\relates <QtTypeTraits>
\since 5.7
\overload
This overload is deleted to prevent a dangling reference in code like
\snippet code/src_corelib_global_qglobal.cpp as-const-4
*/
/*!
\fn template <typename T, typename U = T> T qExchange(T &obj, U &&newValue)
\relates <QtTypeTraits>
\since 5.14
Replaces the value of \a obj with \a newValue and returns the old value of \a obj.
This is Qt's implementation of std::exchange(). It differs from std::exchange()
only in that it is \c constexpr already in C++14, and available on all supported
compilers.
Here is how to use qExchange() to implement move constructors:
\code
MyClass(MyClass &&other)
: m_pointer{qExchange(other.m_pointer, nullptr)},
m_int{qExchange(other.m_int, 0)},
m_vector{std::move(other.m_vector)},
...
\endcode
For members of class type, we can use std::move(), as their move-constructor will
do the right thing. But for scalar types such as raw pointers or integer type, move
is the same as copy, which, particularly for pointers, is not what we expect. So, we
cannot use std::move() for such types, but we can use std::exchange()/qExchange() to
make sure the source object's member is already reset by the time we get to the
initialization of our next data member, which might come in handy if the constructor
exits with an exception.
Here is how to use qExchange() to write a loop that consumes the collection it
iterates over:
\code
for (auto &e : qExchange(collection, {})
doSomethingWith(e);
\endcode
Which is equivalent to the following, much more verbose code:
\code
{
auto tmp = std::move(collection);
collection = {}; // or collection.clear()
for (auto &e : tmp)
doSomethingWith(e);
} // destroys 'tmp'
\endcode
This is perfectly safe, as the for-loop keeps the result of qExchange() alive for as
long as the loop runs, saving the declaration of a temporary variable. Be aware, though,
that qExchange() returns a non-const object, so Qt containers may detach.
*/