From 44b5ca4aa12489c8c250396ccbf6055e7590a1d1 Mon Sep 17 00:00:00 2001 From: Corentin Wallez Date: Wed, 23 May 2018 19:04:03 -0400 Subject: [PATCH] Add a functional-y Result class and tests for it. Implements two optimized versions of Result, one for Result as a nullable pointer, and one for Result as a tagged pointer. --- src/common/CMakeLists.txt | 1 + src/common/Result.h | 250 ++++++++++++++++++++++++++++ src/tests/CMakeLists.txt | 1 + src/tests/unittests/ResultTests.cpp | 137 +++++++++++++++ 4 files changed, 389 insertions(+) create mode 100644 src/common/Result.h create mode 100644 src/tests/unittests/ResultTests.cpp diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 91e8a03f50..db0f642540 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -25,6 +25,7 @@ list(APPEND COMMON_SOURCES ${COMMON_DIR}/Math.cpp ${COMMON_DIR}/Math.h ${COMMON_DIR}/Platform.h + ${COMMON_DIR}/Result.h ${COMMON_DIR}/Serial.h ${COMMON_DIR}/SerialQueue.h ${COMMON_DIR}/SwapChainUtils.h diff --git a/src/common/Result.h b/src/common/Result.h new file mode 100644 index 0000000000..4c7469c678 --- /dev/null +++ b/src/common/Result.h @@ -0,0 +1,250 @@ +// Copyright 2018 The NXT Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef COMMON_RESULT_H_ +#define COMMON_RESULT_H_ + +#include "common/Assert.h" +#include "common/Compiler.h" + +#include +#include + +// Result is the following sum type (Haskell notation): +// +// data Result T E = Success T | Error E | Empty +// +// It is meant to be used as the return type of functions that might fail. The reason for the Empty +// case is that a Result should never be discarded, only destructured (its error or success moved +// out) or moved into a different Result. The Empty case tags Results that have been moved out and +// Result's destructor should ASSERT on it being Empty. +// +// Since C++ doesn't have efficient sum types for the special cases we care about, we provide +// template specializations for them. + +template +class Result; + +// The interface of Result shoud look like the following. +// public: +// Result(T success); +// Result(E error); +// +// Result(Result&& other); +// Result&& operator=(Result&& other); +// +// ~Result(); +// +// bool IsError() const; +// bool IsSuccess() const; +// +// T&& AcquireSuccess(); +// E&& AcquireError(); + +// Specialization of Result for returning errors only via pointers. It is basically a pointer +// where nullptr is both Success and Empty. +template +class NXT_NO_DISCARD Result { + public: + Result(); + Result(E* error); + + Result(Result&& other); + Result& operator=(Result&& other); + + ~Result(); + + bool IsError() const; + bool IsSuccess() const; + + void AcquireSuccess(); + E* AcquireError(); + + private: + E* mError = nullptr; +}; + +// Uses SFINAE to try to get alignof(T) but fallback to Default if T isn't defined. +template +constexpr size_t alignof_if_defined_else_default = Default; + +template +constexpr size_t alignof_if_defined_else_default = alignof(T); + +// Specialization of Result when both the error an success are pointers. It is implemented as a +// tagged pointer. The tag for Success is 0 so that returning the value is fastest. +template +class NXT_NO_DISCARD Result { + public: + static_assert(alignof_if_defined_else_default >= 4, + "Result reserves two bits for tagging pointers"); + static_assert(alignof_if_defined_else_default >= 4, + "Result reserves two bits for tagging pointers"); + + Result(T* success); + Result(E* error); + + Result(Result&& other); + Result& operator=(Result&& other); + + ~Result(); + + bool IsError() const; + bool IsSuccess() const; + + T* AcquireSuccess(); + E* AcquireError(); + + private: + enum PayloadType { + Success = 0, + Error = 1, + Empty = 2, + }; + + // Utility functions to manipulate the tagged pointer some of them don't need to be templated + // but we really want them inlined so we keep them in templates + static intptr_t MakePayload(void* pointer, PayloadType type); + static PayloadType GetPayloadType(intptr_t payload); + static T* GetSuccessFromPayload(intptr_t payload); + static E* GetErrorFromPayload(intptr_t payload); + + constexpr static intptr_t kEmptyPayload = Empty; + intptr_t mPayload = kEmptyPayload; +}; + +// Implementation of Result +template +Result::Result() { +} + +template +Result::Result(E* error) : mError(error) { +} + +template +Result::Result(Result&& other) : mError(other.mError) { + other.mError = nullptr; +} + +template +Result& Result::operator=(Result&& other) { + ASSERT(mError == nullptr); + mError = other.mError; + other.mError = nullptr; + return *this; +} + +template +Result::~Result() { + ASSERT(mError == nullptr); +} + +template +bool Result::IsError() const { + return mError != nullptr; +} + +template +bool Result::IsSuccess() const { + return mError == nullptr; +} + +template +void Result::AcquireSuccess() { +} + +template +E* Result::AcquireError() { + E* error = mError; + mError = nullptr; + return error; +} + +// Implementation of Result +template +Result::Result(T* success) : mPayload(MakePayload(success, Success)) { +} + +template +Result::Result(E* error) : mPayload(MakePayload(error, Error)) { +} + +template +Result::Result(Result&& other) : mPayload(other.mPayload) { + other.mPayload = kEmptyPayload; +} + +template +Result& Result::operator=(Result&& other) { + ASSERT(mPayload == kEmptyPayload); + mPayload = other.mPayload; + other.mPayload = kEmptyPayload; + return *this; +} + +template +Result::~Result() { + ASSERT(mPayload == kEmptyPayload); +} + +template +bool Result::IsError() const { + return GetPayloadType(mPayload) == Error; +} + +template +bool Result::IsSuccess() const { + return GetPayloadType(mPayload) == Success; +} + +template +T* Result::AcquireSuccess() { + T* success = GetSuccessFromPayload(mPayload); + mPayload = kEmptyPayload; + return success; +} + +template +E* Result::AcquireError() { + E* error = GetErrorFromPayload(mPayload); + mPayload = kEmptyPayload; + return error; +} + +template +intptr_t Result::MakePayload(void* pointer, PayloadType type) { + intptr_t payload = reinterpret_cast(pointer); + ASSERT((payload & 3) == 0); + return payload | type; +} + +template +typename Result::PayloadType Result::GetPayloadType(intptr_t payload) { + return static_cast(payload & 3); +} + +template +T* Result::GetSuccessFromPayload(intptr_t payload) { + ASSERT(GetPayloadType(payload) == Success); + return reinterpret_cast(payload); +} + +template +E* Result::GetErrorFromPayload(intptr_t payload) { + ASSERT(GetPayloadType(payload) == Error); + return reinterpret_cast(payload ^ 1); +} + +#endif // COMMON_RESULT_H_ diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 19ddae61cb..4a17398997 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -37,6 +37,7 @@ list(APPEND UNITTEST_SOURCES ${UNITTESTS_DIR}/ObjectBaseTests.cpp ${UNITTESTS_DIR}/PerStageTests.cpp ${UNITTESTS_DIR}/RefCountedTests.cpp + ${UNITTESTS_DIR}/ResultTests.cpp ${UNITTESTS_DIR}/SerialQueueTests.cpp ${UNITTESTS_DIR}/ToBackendTests.cpp ${UNITTESTS_DIR}/WireTests.cpp diff --git a/src/tests/unittests/ResultTests.cpp b/src/tests/unittests/ResultTests.cpp new file mode 100644 index 0000000000..166f7bf304 --- /dev/null +++ b/src/tests/unittests/ResultTests.cpp @@ -0,0 +1,137 @@ +// Copyright 2018 The NXT Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "common/Result.h" + +namespace { + +template +void TestError(Result* result, E expectedError) { + ASSERT_TRUE(result->IsError()); + ASSERT_FALSE(result->IsSuccess()); + + E storedError = result->AcquireError(); + ASSERT_EQ(storedError, expectedError); +} + +template +void TestSuccess(Result* result, T expectedSuccess) { + ASSERT_FALSE(result->IsError()); + ASSERT_TRUE(result->IsSuccess()); + + T storedSuccess = result->AcquireSuccess(); + ASSERT_EQ(storedSuccess, expectedSuccess); +} + +static int dummyError = 0xbeef; +static float dummySuccess = 42.0f; + +// Test constructing an error Result +TEST(ResultOnlyPointerError, ConstructingError) { + Result result(&dummyError); + TestError(&result, &dummyError); +} + +// Test moving an error Result +TEST(ResultOnlyPointerError, MovingError) { + Result result(&dummyError); + Result movedResult(std::move(result)); + TestError(&movedResult, &dummyError); +} + +// Test returning an error Result +TEST(ResultOnlyPointerError, ReturningError) { + auto CreateError = []() -> Result { + return {&dummyError}; + }; + + Result result = CreateError(); + TestError(&result, &dummyError); +} + +// Test constructing a success Result +TEST(ResultOnlyPointerError, ConstructingSuccess) { + Result result; + ASSERT_TRUE(result.IsSuccess()); + ASSERT_FALSE(result.IsError()); +} + +// Test moving a success Result +TEST(ResultOnlyPointerError, MovingSuccess) { + Result result; + Result movedResult(std::move(result)); + ASSERT_TRUE(movedResult.IsSuccess()); + ASSERT_FALSE(movedResult.IsError()); +} + +// Test returning a success Result +TEST(ResultOnlyPointerError, ReturningSuccess) { + auto CreateError = []() -> Result { + return {}; + }; + + Result result = CreateError(); + ASSERT_TRUE(result.IsSuccess()); + ASSERT_FALSE(result.IsError()); +} + +// Test constructing an error Result +TEST(ResultBothPointer, ConstructingError) { + Result result(&dummyError); + TestError(&result, &dummyError); +} + +// Test moving an error Result +TEST(ResultBothPointer, MovingError) { + Result result(&dummyError); + Result movedResult(std::move(result)); + TestError(&movedResult, &dummyError); +} + +// Test returning an error Result +TEST(ResultBothPointer, ReturningError) { + auto CreateError = []() -> Result { + return {&dummyError}; + }; + + Result result = CreateError(); + TestError(&result, &dummyError); +} + +// Test constructing a success Result +TEST(ResultBothPointer, ConstructingSuccess) { + Result result(&dummySuccess); + TestSuccess(&result, &dummySuccess); +} + +// Test moving a success Result +TEST(ResultBothPointer, MovingSuccess) { + Result result(&dummySuccess); + Result movedResult(std::move(result)); + TestSuccess(&movedResult, &dummySuccess); +} + +// Test returning a success Result +TEST(ResultBothPointer, ReturningSuccess) { + auto CreateSuccess = []() -> Result { + return {&dummySuccess}; + }; + + Result result = CreateSuccess(); + TestSuccess(&result, &dummySuccess); +} + +} // anonymous namespace