Add SRB2_ASSERT, srb2::NotNull<T>

Add SRB2_ASSERT, superceding I_Assert

This assertion macro always expands to a call of srb2::do_assert, which
is overloaded with two templates: one which applies if the provided
Level is less than or equal to the SRB2_ASSERTION_LEVEL, and one which
is a no-op. When optimizations are enabled, this will verifiably remove
the evaluation of the expression in all cases, instead of evaluating the
expression and doing nothing with it.

Add srb2::NotNull wrapper utility

This is meant to be used in places where pointers are used as
parameters. It can be used with any pointer-like type, not just raw
pointers. During construction of NotNull, the pointer will be asserted
not-null in debug and paranoia builds, and in release optimizations with
no assertions, the code decays gracefully to standard pointer-passing.
This commit is contained in:
Eidolon 2022-12-28 22:01:47 -06:00
parent fe32cba866
commit 6af003771d
6 changed files with 157 additions and 1 deletions

View file

@ -1,9 +1,12 @@
#ifndef __SRB2_CXXUTIL_HPP__
#define __SRB2_CXXUTIL_HPP__
#include <cstdlib>
#include <type_traits>
#include <utility>
#include "doomdef.h"
namespace srb2 {
template <class F>
@ -19,7 +22,8 @@ public:
void operator=(Finally&& from) = delete;
~Finally() noexcept {
f_();
if (call_)
f_();
}
private:
@ -32,6 +36,117 @@ Finally<std::decay_t<F>> finally(F&& f) noexcept {
return Finally {std::forward<F>(f)};
}
struct SourceLocation {
const char* file_name;
unsigned int line_number;
};
#define SRB2_SOURCE_LOCATION \
srb2::SourceLocation { __FILE__, __LINE__ }
#ifndef SRB2_ASSERT_HANDLER
#define SRB2_ASSERT_HANDLER srb2::IErrorAssertHandler
#endif
#if !defined(NDEBUG) || defined(PARANOIA)
// An assertion level of 2 will activate all invocations of the SRB2_ASSERT macro
#define SRB2_ASSERTION_LEVEL 2
#else
// The minimum assertion level is 1
#define SRB2_ASSERTION_LEVEL 1
#endif
/// Assert a precondition expression in debug builds.
#define SRB2_ASSERT(expr) srb2::do_assert<2, SRB2_ASSERT_HANDLER>([&] { return (expr); }, SRB2_SOURCE_LOCATION, #expr)
class IErrorAssertHandler {
public:
static void handle(const SourceLocation& source_location, const char* expression) {
I_Error("Assertion failed at %s:%u: %s != true",
source_location.file_name,
source_location.line_number,
expression);
}
};
class NoOpAssertHandler {
public:
static void handle(const SourceLocation& source_location, const char* expression) {}
};
/// @brief Assert a precondition expression, aborting the application if it fails.
/// @tparam Expr
/// @tparam Level the level of this assertion; if it is less than or equal to SRB2_ASSERTION_LEVEL, this overload will
/// activate.
/// @param expr a callable which returns a bool
/// @param source_location a struct containing the source location of the assertion, e.g. SRB2_SOURCE_LOCATION
/// @param expression the expression evaluated in the expression callable
/// @param message an optional message to display for the assertion
template <unsigned int Level, class Handler, class Expr>
std::enable_if_t<(Level <= SRB2_ASSERTION_LEVEL), void>
do_assert(const Expr& expr, const SourceLocation& source_location, const char* expression = "") noexcept {
static_assert(Level > 0, "level of an assertion must not be 0");
if (!expr()) {
Handler::handle(source_location, expression);
std::abort();
}
}
template <unsigned int Level, class, class Expr>
std::enable_if_t<(Level > SRB2_ASSERTION_LEVEL), void>
do_assert(const Expr&, const SourceLocation&, const char* = "") noexcept {
}
template <typename T>
class NotNull final {
T ptr_;
public:
static_assert(std::is_convertible_v<decltype(std::declval<T>() != nullptr), bool>,
"T is not comparable with nullptr_t");
/// @brief Move-construct from the pointer value U, asserting that it is not null. Allows construction of a
/// NotNull<T> from any compatible pointer U, for example with polymorphic classes.
template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
constexpr NotNull(U&& rhs) : ptr_(std::forward<U>(rhs)) {
SRB2_ASSERT(ptr_ != nullptr);
}
/// @brief Wrap the pointer type T, asserting that the pointer is not null.
template <typename = std::enable_if_t<!std::is_same_v<std::nullptr_t, T>>>
constexpr NotNull(T rhs) : ptr_(std::move(rhs)) {
SRB2_ASSERT(ptr_ != nullptr);
}
/// @brief Copy construction from NotNull of convertible type U. Only if the incoming pointer is NotNull already.
template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
constexpr NotNull(const NotNull<U>& rhs) : NotNull(rhs.get()) {
// Value is guaranteed to be not null by construction; no assertion necessary
}
NotNull(const NotNull& rhs) = default;
NotNull& operator=(const NotNull& rhs) = default;
/// @brief Get the stored pointer.
constexpr T get() const { return ptr_; }
/// @brief Convert to T (the pointer type).
constexpr operator T() const { return get(); }
/// @brief Arrow-dereference to *T (the actual value pointed to).
constexpr decltype(auto) operator->() const { return get(); }
/// @brief Dereference to *T (the actual value pointed to).
constexpr decltype(auto) operator*() const { return *get(); }
// It is not allowed to construct NotNull<T> with nullptr regardless of T.
NotNull(std::nullptr_t) = delete;
NotNull& operator=(std::nullptr_t) = delete;
};
template <class T>
NotNull(T) -> NotNull<T>;
} // namespace srb2
#endif // __SRB2_CXXUTIL_HPP__

View file

@ -645,6 +645,7 @@ UINT32 quickncasehash (const char *p, size_t n)
#endif
// An assert-type mechanism.
// NOTE: USE SRB2_ASSERT FOR C++ CODE INSTEAD
#ifdef PARANOIA
#define I_Assert(e) ((e) ? (void)0 : I_Error("assert failed: %s, file %s, line %d", #e, __FILE__, __LINE__))
#else

View file

@ -1,3 +1,5 @@
target_sources(srb2tests PRIVATE
boolcompat.cpp
notnull.cpp
testbase.hpp
)

View file

@ -1,3 +1,4 @@
#include "testbase.hpp"
#include <catch2/catch_test_macros.hpp>
#include "../doomtype.h"

31
src/tests/notnull.cpp Normal file
View file

@ -0,0 +1,31 @@
#include "testbase.hpp"
#include <catch2/catch_test_macros.hpp>
#include "../cxxutil.hpp"
namespace {
class A {
public:
virtual bool foo() = 0;
};
class B : public A {
public:
virtual bool foo() override final { return true; };
};
} // namespace
TEST_CASE("NotNull<int*> is constructible from int*") {
int a = 0;
REQUIRE(srb2::NotNull(static_cast<int*>(&a)));
}
TEST_CASE("NotNull<A*> is constructible from B* where B inherits from A") {
B b;
REQUIRE(srb2::NotNull(static_cast<B*>(&b)));
}
TEST_CASE("NotNull<A*> dereferences to B& to call foo") {
B b;
srb2::NotNull<A*> a {static_cast<B*>(&b)};
REQUIRE(a->foo());
}

6
src/tests/testbase.hpp Normal file
View file

@ -0,0 +1,6 @@
#ifndef __SRB2_TESTS_TESTBASE_HPP__
#define __SRB2_TESTS_TESTBASE_HPP__
#define SRB2_ASSERT_HANDLER srb2::NoOpAssertHandler
#endif // __SRB2_TESTS_TESTBASE_HPP__