Ref T485 Update for efficient QString usage in CLogMessage:

- `debug()`, `warning()`, `error()` etc. overloaded on `const char16_t[]` to accept UTF-16 string literals.
- Overloads on `const char *` are deleted to avoid accidents.
- Message is stored as QString and/or QStringView depending which overload is used.
- The multiplexing between QString and QStringView is handled by a special value class, to keep the metaclass simple.
- QStringView does not have `arg()` method, so had to implement our own.
This commit is contained in:
Mat Sutcliffe
2018-12-23 17:12:16 +00:00
parent dd655bcb25
commit cba820cbce
5 changed files with 184 additions and 34 deletions

View File

@@ -67,9 +67,7 @@ namespace BlackMisc
void CLogMessage::preformatted(const CStatusMessage &statusMessage)
{
if (statusMessage.isEmpty()) { return; } // just skip empty messages
CLogMessage msg(statusMessage.getCategories());
msg.m_severity = statusMessage.getSeverity();
msg.m_message = statusMessage.getMessage();
CLogMessage(statusMessage.getCategories()).log(statusMessage.getSeverity(), u"%1") << statusMessage.getMessage();
}
void CLogMessage::preformatted(const CStatusMessageList &statusMessages)

View File

@@ -83,6 +83,7 @@ namespace BlackMisc
CRgbColor::registerMetadata();
CStatusMessage::registerMetadata();
CStatusMessageList::registerMetadata();
CStrongStringView::registerMetadata();
CValueCachePacket::registerMetadata();
CVariant::registerMetadata();
CVariantList::registerMetadata();

View File

@@ -23,35 +23,37 @@ namespace BlackMisc
{
namespace Private
{
namespace
{
template <size_t... Is> QString arg(std::index_sequence<Is...>, const QString &format, const QStringList &args) { return format.arg(args[Is]...); }
QString arg(std::index_sequence<>, const QString &format, const QStringList &) { return format; }
}
QString arg(const QString &format, const QStringList &args)
QString arg(QStringView format, const QStringList &args)
{
if (format.isEmpty())
{
return args.join(" ");
return args.join(u' ');
}
else
thread_local QString temp;
temp.resize(0); // unlike clear(), resize(0) doesn't release the capacity if there are no implicitly shared copies
for (auto it = format.begin(); ; )
{
switch (args.size())
const auto pc = std::find(it, format.end(), u'%');
temp.append(&*it, std::distance(it, pc));
if ((it = pc) == format.end()) { break; }
if (++it == format.end()) { temp += u'%'; break; }
if (*it == u'%') { temp += u'%'; ++it; continue; }
if (is09(*it))
{
case 0: return arg(std::make_index_sequence<0>(), format, args);
case 1: return arg(std::make_index_sequence<1>(), format, args);
case 2: return arg(std::make_index_sequence<2>(), format, args);
case 3: return arg(std::make_index_sequence<3>(), format, args);
case 4: return arg(std::make_index_sequence<4>(), format, args);
case 5: return arg(std::make_index_sequence<5>(), format, args);
case 6: return arg(std::make_index_sequence<6>(), format, args);
case 7: return arg(std::make_index_sequence<7>(), format, args);
case 8: return arg(std::make_index_sequence<8>(), format, args);
default: qWarning("Too many arguments to BlackMisc::Private::arg"); // intentional fall-through
case 9: return arg(std::make_index_sequence<9>(), format, args);
int n = it->unicode() - u'0';
Q_ASSERT(n >= 0 && n <= 9);
if (++it != format.end() && is09(*it)) { n = n * 10 + it->unicode() - u'0'; ++it; }
Q_ASSERT(n >= 0 && n <= 99);
if (n <= args.size()) { temp += args[n - 1]; } else { temp += u'%' % QString::number(n); }
}
else { temp += u'%'; }
}
QString result = temp;
result.squeeze(); // release unused capacity and implicitly detach so temp keeps its capacity for next time
return result;
}
}
@@ -235,13 +237,13 @@ namespace BlackMisc
void CStatusMessage::prependMessage(const QString &msg)
{
if (msg.isEmpty()) { return; }
m_message = msg + m_message;
m_message = QString(msg % m_message.view());
}
void CStatusMessage::appendMessage(const QString &msg)
{
if (msg.isEmpty()) { return; }
m_message += msg;
m_message = QString(m_message.view() % msg);
}
void CStatusMessage::markAsHandledBy(const QObject *object) const

View File

@@ -27,8 +27,8 @@ namespace BlackMisc
namespace Private
{
//! Like QString::arg() but accepts a QStringList of args.
BLACKMISC_EXPORT QString arg(const QString &format, const QStringList &args);
//! Like QString::arg() but accepts a QVector of args.
BLACKMISC_EXPORT QString arg(QStringView format, const QStringList &args);
}
/*!
@@ -42,6 +42,81 @@ namespace BlackMisc
SeverityError
};
/*!
* Special-purpose string class used by CMessageBase.
*
* Wraps a QStringView that can be constructed from a UTF-16 string literal or from a QString.
* If constructed from a QString, the QString is stored to prevent a dangling pointer.
*/
class CStrongStringView :
public Mixin::MetaType<CStrongStringView>,
public Mixin::EqualsByCompare<CStrongStringView>,
public Mixin::LessThanByCompare<CStrongStringView>,
public Mixin::DBusOperators<CStrongStringView>,
public Mixin::JsonOperators<CStrongStringView>
{
public:
//! Default constructor.
CStrongStringView() = default;
//! Construct from a UTF-16 character array.
template <size_t N>
CStrongStringView(const char16_t (&string)[N]) : m_view(string) {}
//! Construct from a QString.
CStrongStringView(const QString &string) : m_string(string), m_view(m_string) {}
//! Deleted constructor.
CStrongStringView(const char *) = delete;
//! Copy constructor.
CStrongStringView(const CStrongStringView &other) { *this = other; }
//! Copy assignment operator.
CStrongStringView &operator =(const CStrongStringView &other)
{
if (other.isOwning()) { m_view = m_string = other.m_string; } else { m_view = other.m_view; m_string.clear(); }
return *this;
}
//! Destructor.
~CStrongStringView() = default;
//! String is empty.
bool isEmpty() const { return view().isEmpty(); }
//! Does it own its string data?
bool isOwning() const { return !m_string.isNull(); } // important distinction between isNull and isEmpty
//! Return as a QStringView.
QStringView view() const { return m_view; }
//! Return a copy as a QString.
QString toQString(bool i18n) const { Q_UNUSED(i18n); return isOwning() ? m_string : m_view.toString(); }
//! Compare two strings.
friend int compare(const CStrongStringView &a, const CStrongStringView &b) { return a.m_view.compare(b.m_view); }
//! Hash value.
friend uint qHash(const CStrongStringView &obj, uint seed = 0) { return ::qHash(obj.m_view, seed); }
//! DBus marshalling.
//! @{
void marshallToDbus(QDBusArgument &arg) const { if (isOwning()) { arg << m_string; } else { arg << m_view.toString(); } }
void unmarshallFromDbus(const QDBusArgument &arg) { QString s; arg >> s; *this = s; }
//! @}
//! JSON conversion.
//! @{
QJsonObject toJson() const { QJsonObject json; json.insert(QStringLiteral("value"), m_view.toString()); return json; }
void convertFromJson(const QJsonObject &json) { *this = json.value(QLatin1String("value")).toString(); }
//! @}
private:
QString m_string;
QStringView m_view;
};
/*!
* Base class for CStatusMessage and CLogMessage.
*/
@@ -65,34 +140,83 @@ namespace BlackMisc
CMessageBase(const CLogCategoryList &categories, const CLogCategoryList &extra) : CMessageBase(categories) { this->addIfNotExisting(extra); }
//! Set the severity and format string.
//! @{
template <size_t N>
Derived &log(StatusSeverity s, const char16_t (&m)[N]) { m_message = m; m_severity = s; return derived(); }
Derived &log(StatusSeverity s, const QString &m) { m_message = m; m_severity = s; return derived(); }
//! @}
//! Set the severity to debug.
Derived &debug() { return log(SeverityDebug, ""); }
Derived &debug() { return log(SeverityDebug, QString()); }
//! Set the severity to debug, providing a format string.
//! @{
template <size_t N>
Derived &debug(const char16_t (&format)[N]) { return log(SeverityDebug, format); }
Derived &debug(const QString &format) { return log(SeverityDebug, format); }
//! @}
//! Set the severity to info, providing a format string.
//! @{
template <size_t N>
Derived &info(const char16_t (&format)[N]) { return log(SeverityInfo, format); }
Derived &info(const QString &format) { return log(SeverityInfo, format); }
//! @}
//! Set the severity to warning, providing a format string.
//! @{
template <size_t N>
Derived &warning(const char16_t (&format)[N]) { return log(SeverityWarning, format); }
Derived &warning(const QString &format) { return log(SeverityWarning, format); }
//! @}
//! Set the severity to error, providing a format string.
//! @{
template <size_t N>
Derived &error(const char16_t (&format)[N]) { return log(SeverityError, format); }
Derived &error(const QString &format) { return log(SeverityError, format); }
//! @}
//! Set the severity to s, providing a format string, and adding the validation category.
//! @{
template <size_t N>
Derived &validation(StatusSeverity s, const char16_t (&format)[N]) { setValidation(); return log(s, format); }
Derived &validation(StatusSeverity s, const QString &format) { setValidation(); return log(s, format); }
//! @}
//! Set the severity to info, providing a format string, and adding the validation category.
//! @{
template <size_t N>
Derived &validationInfo(const char16_t (&format)[N]) { setValidation(); return log(SeverityInfo, format); }
Derived &validationInfo(const QString &format) { setValidation(); return log(SeverityInfo, format); }
//! @}
//! Set the severity to warning, providing a format string, and adding the validation category.
//! @{
template <size_t N>
Derived &validationWarning(const char16_t (&format)[N]) { setValidation(); return log(SeverityWarning, format); }
Derived &validationWarning(const QString &format) { setValidation(); return log(SeverityWarning, format); }
//! @}
//! Set the severity to error, providing a format string, and adding the validation category.
//! @{
template <size_t N>
Derived &validationError(const char16_t (&format)[N]) { setValidation(); return log(SeverityError, format); }
Derived &validationError(const QString &format) { setValidation(); return log(SeverityError, format); }
//! @}
//! Deleted methods to avoid accidental implicit conversion from Latin-1 or UTF-8 string literals.
//! @{
Derived &log(StatusSeverity, const char *) = delete;
Derived &debug(const char *) = delete;
Derived &info(const char *) = delete;
Derived &warning(const char *) = delete;
Derived &error(const char *) = delete;
Derived &validation(StatusSeverity, const char *) = delete;
Derived &validationInfo(const char *) = delete;
Derived &validationWarning(const char *) = delete;
Derived &validationError(const char *) = delete;
//! @}
//! Streaming operators.
//! \details If the format string is empty, the message will consist of all streamed values separated by spaces.
@@ -115,6 +239,9 @@ namespace BlackMisc
Derived & operator <<(const T &v) { return arg(v.toQString()); }
//! @}
//! Message empty
bool isEmpty() const { return this->m_message.isEmpty() && this->m_args.isEmpty(); }
private:
void setValidation() { m_categories.remove(CLogCategory::uncategorized()); this->addIfNotExisting(CLogCategory::validation()); }
Derived &arg(const QString &value) { m_args.push_back(value); return derived(); }
@@ -144,14 +271,15 @@ namespace BlackMisc
}
}
protected:
//! \private
//! @{
QString m_message;
CStrongStringView m_message;
QStringList m_args;
CLogCategoryList m_categories = CLogCategoryList { CLogCategory::uncategorized() };
StatusSeverity m_severity = SeverityDebug;
QString message() const { return Private::arg(m_message, m_args); }
QString message() const { return Private::arg(m_message.view(), m_args); }
//! @}
};
@@ -281,9 +409,6 @@ namespace BlackMisc
//! Append message
void appendMessage(const QString &msg);
//! Message empty
bool isEmpty() const { return this->m_message.isEmpty() && this->m_args.isEmpty(); }
//! Returns true if this message was sent by an instance of class T.
template <class T>
bool isFromClass(const T *pointer = nullptr) const
@@ -409,6 +534,7 @@ namespace BlackMisc
}
} // namespace
Q_DECLARE_METATYPE(BlackMisc::CStrongStringView)
Q_DECLARE_METATYPE(BlackMisc::CStatusMessage)
Q_DECLARE_METATYPE(BlackMisc::CStatusMessage::StatusSeverity)

View File

@@ -28,6 +28,8 @@ namespace BlackMiscTest
private slots:
//! Status message
void statusMessage();
//! Message with arguments
void statusArgs();
};
void CTestStatusMessage::statusMessage()
@@ -48,6 +50,27 @@ namespace BlackMiscTest
cAssign = s2;
QVERIFY(cAssign.getMSecsSinceEpoch() == s2.getMSecsSinceEpoch());
}
void CTestStatusMessage::statusArgs()
{
auto s1 = CStatusMessage().info(u"literal percent: %1");
auto s2 = CStatusMessage().info(u"literal percent: %a");
auto s3 = CStatusMessage().info(u"literal percent: %");
auto s4 = CStatusMessage().info(u"literal percent: %%");
auto s5 = CStatusMessage().info(u"literal percents: %%%");
auto s6 = CStatusMessage().info(u"will be expanded: %1%2") << "foo" << "bar";
auto s7 = CStatusMessage().info(u"will be expanded: %1+%2") << "foo" << "bar";
auto s8 = CStatusMessage().info(u"will be expanded: %012") << "foo";
QVERIFY(s1.getMessage() == "literal percent: %1");
QVERIFY(s2.getMessage() == "literal percent: %a");
QVERIFY(s3.getMessage() == "literal percent: %");
QVERIFY(s4.getMessage() == "literal percent: %");
QVERIFY(s5.getMessage() == "literal percents: %%");
QVERIFY(s6.getMessage() == "will be expanded: foobar");
QVERIFY(s7.getMessage() == "will be expanded: foo+bar");
QVERIFY(s8.getMessage() == "will be expanded: foo2");
}
} // namespace
//! main