diff options
-rw-r--r-- | .github/workflows/ci.yml | 41 | ||||
-rw-r--r-- | README.md | 41 | ||||
-rwxr-xr-x | autotests/run-tests.sh | 6 | ||||
-rw-r--r-- | lib/events/event.h | 64 | ||||
-rw-r--r-- | lib/function_traits.cpp | 3 | ||||
-rw-r--r-- | lib/function_traits.h | 36 | ||||
-rw-r--r-- | lib/qt_connection_util.h | 170 |
7 files changed, 170 insertions, 191 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d619385f..f84356b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,12 +47,12 @@ jobs: compiler: LLVM e2ee: e2ee static-analysis: codeql - - os: ubuntu-latest + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: GCC e2ee: e2ee static-analysis: sonar - - os: ubuntu-20.04 + - os: ubuntu-22.04 qt-version: '5.15.2' compiler: GCC e2ee: e2ee @@ -82,11 +82,30 @@ jobs: with: fetch-depth: 0 + - name: Cache Qt + id: cache-qt + uses: actions/cache@v2 + with: + path: ${{ runner.workspace }}/Qt + key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache + + - name: Install Qt + uses: jurplel/install-qt-action@v2.14.0 + with: + version: ${{ matrix.qt-version }} + arch: ${{ matrix.qt-arch }} + cached: ${{ steps.cache-qt.outputs.cache-hit }} + - name: Setup build environment run: | if [ '${{ matrix.compiler }}' == 'GCC' ]; then - echo "CC=gcc-10" >>$GITHUB_ENV - echo "CXX=g++-10" >>$GITHUB_ENV + echo "CC=gcc" >>$GITHUB_ENV + echo "CXX=g++" >>$GITHUB_ENV + if [ '${{ startsWith(matrix.qt-version, '5') }}' == 'true' ]; then + # Patch Qt to avoid GCC tumbling over QTBUG-90568/QTBUG-91909 + sed -i 's/ThreadEngineStarter<void>(ThreadEngine<void> \*_threadEngine)/ThreadEngineStarter(ThreadEngine<void> \*_threadEngine)/' \ + $Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h + fi elif [[ '${{ runner.os }}' != 'Windows' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV @@ -124,20 +143,6 @@ jobs: cmake -E make_directory ${{ runner.workspace }}/build echo "BUILD_PATH=${{ runner.workspace }}/build/libQuotient" >>$GITHUB_ENV - - name: Cache Qt - id: cache-qt - uses: actions/cache@v2 - with: - path: ${{ runner.workspace }}/Qt - key: ${{ runner.os }}${{ matrix.platform }}-Qt${{ matrix.qt-version }}-cache - - - name: Install Qt - uses: jurplel/install-qt-action@v2.14.0 - with: - version: ${{ matrix.qt-version }} - arch: ${{ matrix.qt-arch }} - cached: ${{ steps.cache-qt.outputs.cache-hit }} - - name: Install Ninja (macOS/Windows) if: ${{ !startsWith(matrix.os, 'ubuntu') }} uses: seanmiddleditch/gha-setup-ninja@v3 @@ -43,7 +43,7 @@ your application, as described below. - CMake 3.16 or newer (from your package management system or [the official website](https://cmake.org/download/)) - A C++ toolchain with that supports at least some subset of C++20: - - GCC 10 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) + - GCC 11 (Windows, Linux, macOS), Clang 11 (Linux), Apple Clang 12 (macOS) and Visual Studio 2019 (Windows) are the oldest officially supported. - Any build system that works with CMake should be fine: GNU Make and ninja on any platform, NMake and jom on Windows are known to work. @@ -165,14 +165,37 @@ by setting `Quotient_INSTALL_TESTS` to `OFF`. #### Building fails -If `cmake` fails with... -``` -CMake Warning at CMakeLists.txt:11 (find_package): - By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project - has asked CMake to find a package configuration file provided by - "Qt5Widgets", but CMake did not find one. -``` -...then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. +- If `cmake` fails with + ``` + CMake Warning at CMakeLists.txt:11 (find_package): + By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project + has asked CMake to find a package configuration file provided by + "Qt5Widgets", but CMake did not find one. + ``` + then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. + +- If you use GCC and get an "unknown declarator" compilation error in the file +`qtconcurrentthreadengine.h` - unfortunately, it is an actual error in Qt 5.15 +sources, see https://bugreports.qt.io/browse/QTBUG-90568 (or +https://bugreports.qt.io/browse/QTBUG-91909). The Qt company did not make +an open source release with the fix, therefore: + + - if you're on Linux - try to use Qt from your package management system, as + most likely this bug is already fixed in the packages + - if you're on Windows, or if you have to use Qt (5.15) from download.qt.io + for any other reason, you should apply the fix to Qt sources: locate + the file (the GCC error message tells exactly where it is), find the line + with the (strange-looking) `ThreadEngineStarter` constructor definition: + ```cplusplus + ThreadEngineStarter<void>(ThreadEngine<void> \*_threadEngine) + ``` + and remove the template specialisation from the constructor name so that it + looks like + ```cplusplus + ThreadEngineStarter(ThreadEngine<void> \*_threadEngine) + ``` + This will fix your build (and any other build involving QtConcurrent from + this installation of Qt - the fix is not specific to Quotient in any way). #### Logging configuration diff --git a/autotests/run-tests.sh b/autotests/run-tests.sh index 05a215af..e7a228ef 100755 --- a/autotests/run-tests.sh +++ b/autotests/run-tests.sh @@ -1,16 +1,18 @@ mkdir -p data chmod 0777 data +SYNAPSE_IMAGE='matrixdotorg/synapse:v1.61.1' + rm ~/.local/share/testolmaccount -rf docker run -v `pwd`/data:/data --rm \ - -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate + -e SYNAPSE_SERVER_NAME=localhost -e SYNAPSE_REPORT_STATS=no $SYNAPSE_IMAGE generate (cd data && . ../autotests/adjust-config.sh) docker run -d \ --name synapse \ -p 1234:8008 \ -p 8448:8008 \ -p 8008:8008 \ - -v `pwd`/data:/data matrixdotorg/synapse:latest + -v `pwd`/data:/data $SYNAPSE_IMAGE trap "rm -rf ./data/*; docker rm -f synapse 2>&1 >/dev/null; trap - EXIT" EXIT echo Waiting for synapse to start... diff --git a/lib/events/event.h b/lib/events/event.h index ec21c6aa..05eb51e9 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -341,56 +341,34 @@ inline auto eventCast(const BasePtrT& eptr) : nullptr; } -// A trivial generic catch-all "switch" -template <class BaseEventT, typename FnT> -inline auto switchOnType(const BaseEventT& event, FnT&& fn) - -> decltype(fn(event)) -{ - return fn(event); -} - namespace _impl { - // Using bool instead of auto below because auto apparently upsets MSVC - template <class BaseT, typename FnT> - constexpr bool needs_downcast = - std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>> - && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>; -} - -// A trivial type-specific "switch" for a void function -template <class BaseT, typename FnT> -inline auto switchOnType(const BaseT& event, FnT&& fn) - -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT> - && std::is_void_v<fn_return_t<FnT>>> -{ - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - fn(static_cast<event_type>(event)); + template <typename FnT, class BaseT> + concept Invocable_With_Downcast = + std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>; } -// A trivial type-specific "switch" for non-void functions with an optional -// default value; non-voidness is guarded by defaultValue type -template <class BaseT, typename FnT> -inline auto switchOnType(const BaseT& event, FnT&& fn, - fn_return_t<FnT>&& defaultValue = {}) - -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>> +template <class BaseT, typename TailT> +inline auto switchOnType(const BaseT& event, TailT&& tail) { - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - return fn(static_cast<event_type>(event)); - return std::move(defaultValue); + if constexpr (std::is_invocable_v<TailT, BaseT>) { + return tail(event); + } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) { + using event_type = fn_arg_t<TailT>; + if (is<std::decay_t<event_type>>(event)) + return tail(static_cast<event_type>(event)); + return std::invoke_result_t<TailT, event_type>(); // Default-constructed + } else { // Treat it as a value to return + return std::forward<TailT>(tail); + } } -// A switch for a chain of 2 or more functions -template <class BaseT, typename FnT1, typename FnT2, typename... FnTs> -inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> -switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) +template <class BaseT, typename FnT1, typename... FnTs> +inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) { using event_type1 = fn_arg_t<FnT1>; if (is<std::decay_t<event_type1>>(event)) - return fn1(static_cast<event_type1&>(event)); - return switchOnType(event, std::forward<FnT2>(fn2), - std::forward<FnTs>(fns)...); + return fn1(static_cast<event_type1>(event)); + return switchOnType(event, std::forward<FnTs>(fns)...); } template <class BaseT, typename... FnTs> @@ -405,8 +383,8 @@ inline auto visit(const BaseT& event, FnTs&&... fns) // TODO: replace with ranges::for_each once all standard libraries have it template <typename RangeT, typename... FnTs> inline auto visitEach(RangeT&& events, FnTs&&... fns) - -> std::enable_if_t<std::is_void_v< - decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>> + requires std::is_void_v< + decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))> { for (auto&& evtPtr: events) switchOnType(*evtPtr, std::forward<FnTs>(fns)...); diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp index 6542101a..e3d27122 100644 --- a/lib/function_traits.cpp +++ b/lib/function_traits.cpp @@ -7,6 +7,9 @@ using namespace Quotient; +template <typename FnT> +using fn_return_t = typename function_traits<FnT>::return_type; + int f_(); static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>, "Test fn_return_t<>"); diff --git a/lib/function_traits.h b/lib/function_traits.h index 83b8e425..143ed162 100644 --- a/lib/function_traits.h +++ b/lib/function_traits.h @@ -8,7 +8,7 @@ namespace Quotient { namespace _impl { - template <typename AlwaysVoid, typename> + template <typename> struct fn_traits {}; } @@ -21,73 +21,73 @@ namespace _impl { */ template <typename T> struct function_traits - : public _impl::fn_traits<void, std::remove_reference_t<T>> {}; + : public _impl::fn_traits<std::remove_reference_t<T>> {}; // Specialisation for a function template <typename ReturnT, typename... ArgTs> struct function_traits<ReturnT(ArgTs...)> { using return_type = ReturnT; using arg_types = std::tuple<ArgTs...>; - // See also the comment for wrap_in_function() in qt_connection_util.h - using function_type = std::function<ReturnT(ArgTs...)>; }; namespace _impl { - template <typename AlwaysVoid, typename> + template <typename> struct fn_object_traits; // Specialisation for a lambda function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...)> : function_traits<ReturnT(ArgTs...)> {}; // Specialisation for a const lambda function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...) const> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...) const> : function_traits<ReturnT(ArgTs...)> {}; // Specialisation for function objects with (non-overloaded) operator() // (this includes non-generic lambdas) template <typename T> - struct fn_traits<decltype(void(&T::operator())), T> - : public fn_object_traits<void, decltype(&T::operator())> {}; + requires requires { &T::operator(); } + struct fn_traits<T> + : public fn_object_traits<decltype(&T::operator())> {}; // Specialisation for a member function in a non-functor class template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...)> : function_traits<ReturnT(ClassT, ArgTs...)> {}; // Specialisation for a const member function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...) const> : function_traits<ReturnT(const ClassT&, ArgTs...)> {}; // Specialisation for a constref member function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const&> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...) const&> : function_traits<ReturnT(const ClassT&, ArgTs...)> {}; // Specialisation for a prvalue member function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) &&> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...) &&> : function_traits<ReturnT(ClassT&&, ArgTs...)> {}; // Specialisation for a pointer-to-member template <typename ReturnT, typename ClassT> - struct fn_traits<void, ReturnT ClassT::*> + struct fn_traits<ReturnT ClassT::*> : function_traits<ReturnT&(ClassT)> {}; // Specialisation for a const pointer-to-member template <typename ReturnT, typename ClassT> - struct fn_traits<void, const ReturnT ClassT::*> + struct fn_traits<const ReturnT ClassT::*> : function_traits<const ReturnT&(ClassT)> {}; } // namespace _impl -template <typename FnT> -using fn_return_t = typename function_traits<FnT>::return_type; - template <typename FnT, int ArgN = 0> using fn_arg_t = std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; +template <typename FnT> +constexpr auto fn_arg_count_v = + std::tuple_size_v<typename function_traits<FnT>::arg_types>; + } // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 86593cc8..90bc3f9b 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -9,101 +9,68 @@ namespace Quotient { namespace _impl { - template <typename... ArgTs> - using decorated_slot_tt = - std::function<void(QMetaObject::Connection&, const ArgTs&...)>; + enum ConnectionType { SingleShot, Until }; - template <typename SenderT, typename SignalT, typename ContextT, typename... ArgTs> - inline QMetaObject::Connection - connectDecorated(SenderT* sender, SignalT signal, ContextT* context, - decorated_slot_tt<ArgTs...> decoratedSlot, - Qt::ConnectionType connType) + template <ConnectionType CType> + inline auto connect(auto* sender, auto signal, auto* context, auto slotLike, + Qt::ConnectionType connType) { - auto pc = std::make_unique<QMetaObject::Connection>(); - auto& c = *pc; // Resolve a reference before pc is moved to lambda - - // Perfect forwarding doesn't work through signal-slot connections - - // arguments are always copied (at best - COWed) to the context of - // the slot. Therefore the slot decorator receives const ArgTs&... - // rather than ArgTs&&... - // TODO (C++20): std::bind_front() instead of lambda. - c = QObject::connect(sender, signal, context, - [pc = std::move(pc), - decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { - Q_ASSERT(*pc); // If it's been triggered, it should exist - decoratedSlot(*pc, args...); + std::unique_ptr<QMetaObject::Connection> pConn = + std::make_unique<QMetaObject::Connection>(); + auto& c = *pConn; // Save the reference before pConn is moved from + c = QObject::connect( + sender, signal, context, + [slotLike, pConn = std::move(pConn)](const auto&... args) + // The requires-expression below is necessary to prevent Qt + // from eagerly trying to fill the lambda with more arguments + // than slotLike() (i.e., the original slot) can handle + requires requires { slotLike(args...); } { + static_assert(CType == Until || CType == SingleShot, + "Unsupported disconnection type"); + if constexpr (CType == SingleShot) { + // Disconnect early to avoid re-triggers during slotLike() + QObject::disconnect(*pConn); + // Qt kindly keeps slot objects until they do their job, + // even if they disconnect themselves in the process (see + // how doActivate() in qobject.cpp handles c->slotObj). + slotLike(args...); + } else if constexpr (CType == Until) { + if (slotLike(args...)) + QObject::disconnect(*pConn); + } }, connType); return c; } - template <typename SenderT, typename SignalT, typename ContextT, - typename... ArgTs> - inline QMetaObject::Connection - connectUntil(SenderT* sender, SignalT signal, ContextT* context, - std::function<bool(ArgTs...)> functor, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt<ArgTs...>( - [functor = std::move(functor)](QMetaObject::Connection& c, - const ArgTs&... args) { - if (functor(args...)) - QObject::disconnect(c); - }), - connType); - } - template <typename SenderT, typename SignalT, typename ContextT, - typename... ArgTs> - inline QMetaObject::Connection - connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, - std::function<void(ArgTs...)> slot, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt<ArgTs...>( - [slot = std::move(slot)](QMetaObject::Connection& c, - const ArgTs&... args) { - QObject::disconnect(c); - slot(args...); - }), - connType); - } - // TODO: get rid of it as soon as Apple Clang gets proper deduction guides - // for std::function<> - // ...or consider using QtPrivate magic used by QObject::connect() - // ...for inspiration, also check a possible std::not_fn implementation - // at https://en.cppreference.com/w/cpp/utility/functional/not_fn - template <typename FnT> - inline auto wrap_in_function(FnT&& f) - { - return typename function_traits<FnT>::function_type(std::forward<FnT>(f)); - } + template <typename SlotT, typename ReceiverT> + concept PmfSlot = + (fn_arg_count_v<SlotT> > 0 + && std::is_base_of_v<std::decay_t<fn_arg_t<SlotT, 0>>, ReceiverT>); } // namespace _impl -/*! \brief Create a connection that self-disconnects when its "slot" returns true - * - * A slot accepted by connectUntil() is different from classic Qt slots - * in that its return value must be bool, not void. The slot's return value - * controls whether the connection should be kept; if the slot returns false, - * the connection remains; upon returning true, the slot is disconnected from - * the signal. Because of a different slot signature connectUntil() doesn't - * accept member functions as QObject::connect or Quotient::connectSingleShot - * do; you should pass a lambda or a pre-bound member function to it. - */ -template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> -inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, - const FunctorT& slot, +//! \brief Create a connection that self-disconnects when its slot returns true +//! +//! A slot accepted by connectUntil() is different from classic Qt slots +//! in that its return value must be bool, not void. Because of that different +//! signature connectUntil() doesn't accept member functions in the way +//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda +//! or a pre-bound member function to it. +//! \return whether the connection should be dropped; false means that the +//! connection remains; upon returning true, the slot is disconnected +//! from the signal. +inline auto connectUntil(auto* sender, auto signal, auto* context, + auto smartSlot, Qt::ConnectionType connType = Qt::AutoConnection) { - return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot), - connType); + return _impl::connect<_impl::Until>(sender, signal, context, smartSlot, + connType); } -/// Create a connection that self-disconnects after triggering on the signal -template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ContextT* context, const FunctorT& slot, +//! Create a connection that self-disconnects after triggering on the signal +template <typename ContextT, typename SlotT> +inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, + SlotT slot, Qt::ConnectionType connType = Qt::AutoConnection) { #if QT_VERSION_MAJOR >= 6 @@ -111,25 +78,26 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, Qt::ConnectionType(connType | Qt::SingleShotConnection)); #else - return _impl::connectSingleShot( - sender, signal, context, _impl::wrap_in_function(slot), connType); -} - -// Specialisation for usual Qt slots passed as pointers-to-members. -template <typename SenderT, typename SignalT, typename ReceiverT, - typename SlotObjectT, typename... ArgTs> -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ReceiverT* receiver, - void (SlotObjectT::*slot)(ArgTs...), - Qt::ConnectionType connType = Qt::AutoConnection) -{ - // TODO: when switching to C++20, use std::bind_front() instead - return _impl::connectSingleShot(sender, signal, receiver, - _impl::wrap_in_function( - [receiver, slot](const ArgTs&... args) { - (receiver->*slot)(args...); - }), - connType); + // In case of classic Qt pointer-to-member-function slots the receiver + // object has to be pre-bound to the slot to make it self-contained + if constexpr (_impl::PmfSlot<SlotT, ContextT>) { + auto&& boundSlot = +# if __cpp_lib_bind_front // Needs Apple Clang 13 (other platforms are fine) + std::bind_front(slot, context); +# else + [context, slot](const auto&... args) + requires requires { (context->*slot)(args...); } + { + (context->*slot)(args...); + }; +# endif + return _impl::connect<_impl::SingleShot>( + sender, signal, context, + std::forward<decltype(boundSlot)>(boundSlot), connType); + } else { + return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, + connType); + } #endif } |