Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
test_library:
uses: NWChemEx/.github/.github/workflows/test_nwx_library.yaml@master
with:
compilers: '["gcc-11", "clang-14"]'
compilers: '["gcc-14", "clang-18"]'
8 changes: 4 additions & 4 deletions cmake/catch2_tests_from_dir.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ include_guard()

function(catch2_tests_from_dir ctfd_target_name ctfd_dir)
file(GLOB_RECURSE ctfd_test_files CONFIGURE_DEPENDS ${ctfd_dir}/*.cpp)

add_executable(${ctfd_target_name} ${ctfd_test_files})

target_link_libraries(
${ctfd_target_name} PRIVATE Catch2::Catch2WithMain ${ARGN})

add_test(NAME ${ctfd_target_name}
COMMAND ${ctfd_target_name}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endfunction()
endfunction()
2 changes: 1 addition & 1 deletion cmake/disable_in_source_builds.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ function(disable_in_source_builds)
endif()
endfunction()

disable_in_source_builds()
disable_in_source_builds()
50 changes: 25 additions & 25 deletions docs/source/developer/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Performing Type-Safe Operations
###############################

A key challenge in the design of WTF is how to perform arbitrary numeric
A key challenge in the design of WTF is how to perform arbitrary numeric
operations in a performant and type-safe manner.

***************
Expand All @@ -33,10 +33,10 @@ something like:
void run()(T val) {
// do something with val
}
};
};

wtf::Buffer b = fxn_that_returns_a_buffer_of_wtf_floats();
b.evaluate(MyFunctor{});
b.evaluate(MyFunctor{});

can be implemented by having the ``evaluate`` function call a virtual function
on the holder. The model class, then implements ``evaluate`` by calling the
Expand Down Expand Up @@ -67,17 +67,17 @@ a time (the type of ``*this``), so we need a different solution.
What won't work?
================

At this point note that though ``std::variant`` is a common C++ solution for
At this point note that though ``std::variant`` is a common C++ solution for
multiple dispatch. Instead of using the standard type-erasure pattern we'd
hold the value in ``std::variant`` (the ``std::variant`` would live in the
the class that is currently the interface or the class that is the holder and
the model class would no longer be needed). However, this solution only works if
we know ALL of the types ahead of time. Alternatively, we could template the
interface on the ``std::variant`` (or the types in the ``std::variant``) to
interface on the ``std::variant`` (or the types in the ``std::variant``) to
avoid assuming a set of types, but this then destroys the type-erasure WTF is
trying to implement since the interface is now templated!

Another common solution is the visitor pattern, but ultimately that
Another common solution is the visitor pattern, but ultimately that
will require a class like:

.. code-block:: c++
Expand All @@ -93,39 +93,39 @@ will require a class like:
virtual void visit(long double, double) = 0;
virtual void visit(long double, long double) = 0;
};

// With some template trickery we could then get the following to work:
Visitor& v = ...; // User-defined derived class passed by base
wtf::Buffer val0;
wtf::Buffer val1;
dispatch(v, val0, val1);
dispatch(v, val0, val1);

(n.b., for double dispatch we can make the visitor's interface linear in the
number of FP types by relying on polymorphism to work out one of the types;
however, it will still result in a combinatorial explosion for dispatching on
three or more inputs). This suffers from the same problem as ``std::variant``:
we don't want to assume a list of FP types (in fact, as the name ``std::visit``
suggest, the two are related...).
(n.b., for double dispatch we can make the visitor's interface linear in the
number of FP types by relying on polymorphism to work out one of the types;
however, it will still result in a combinatorial explosion for dispatching on
three or more inputs). This suffers from the same problem as ``std::variant``:
we don't want to assume a list of FP types (in fact, as the name ``std::visit``
suggest, the two are related...).


FWIW, there's a name for wanting to have multiple dispatch while not wanting to
FWIW, there's a name for wanting to have multiple dispatch while not wanting to
assume a list of types: it's called the "expression problem".

Solving the Expression Problem
==============================

At present, solving the expression problem requires a mix of storing RTTI
(runtime type information) and brute force looping over class hierarchies.
At present, solving the expression problem requires a mix of storing RTTI
(runtime type information) and brute force looping over class hierarchies.
Making sure that all the edge cases are handled correctly is tricky and tedious.
Note that this all needs to be done with template meta-programming, so it's also
hideous to look at. So let's see if there is an existing solution that avoids
us needing to reinvent the wheel.

One of the first solutions we found was the YOMM2 library (I'm guessing it
stands for yorel open multi-method; not sure what yorel means...). Yomm2
provides a C++17 solution to the expression problem. Based on preliminary
investigations it suffers from weird scoping rules. More specifically, if you
define everything in one file it works, but if you try to split it up into
One of the first solutions we found was the YOMM2 library (I'm guessing it
stands for yorel open multi-method; not sure what yorel means...). Yomm2
provides a C++17 solution to the expression problem. Based on preliminary
investigations it suffers from weird scoping rules. More specifically, if you
define everything in one file it works, but if you try to split it up into
multiple files you can break it. Given that it's all native C++, the scope rules
that Yomm2 ultimately follows are that of C++ itself. However, Yomm2 is a
complicated web of C macros and template meta-programming making it quite
Expand All @@ -140,11 +140,11 @@ finite set of floating point types and that they know what those types are. If
that is the case, the user can provide us with the list of types they support
when they want to dispatch. WTF can then use that list to create a series of
``std::variant`` objects for the operation, use ``std::visit`` to do the
multiple dispatch, and then return the result. By comparison, the usual
``std::variant`` solution forces the same set of types on all type-erased
multiple dispatch, and then return the result. By comparison, the usual
``std::variant`` solution forces the same set of types on all type-erased
objects in the hierarchy and on all operations using those types. Our solution
still allows the objects to erase arbitrary floating point types, but now
restricts each execution of an operation to a set of types. Of note, each time
an operation is invoked it can be invoked with a different set of types. Of
course, if the held floating-point type is not convertible to one of the types
in the set, an exception will be thrown at runtime.
in the set, an exception will be thrown at runtime.
14 changes: 7 additions & 7 deletions docs/source/developer/statement_of_need.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@ used.
values, we just don't want to keep bringing it up.

A fundamental problem in the software engineering of scientific libraries is
dealing with FP types. Historically, for simplicity many scientific libraries
dealing with FP types. Historically, for simplicity many scientific libraries
have assumed ``double`` as the FP type at all interfaces. If the user is
storing their data as ``float``, they must convert it to ``double`` to call the
interface, and then convert the result back to ``float`` after the call.
Admittedly, this is why many scientific libraries provide overloads for other
FP types, but this in turn requires the developer to maintain one interface
per FP type.
per FP type.

C++ libraries can avoid the need to support multiple overloads by using
templates. As long as the user of the library is calling the function from C++,
C++ libraries can avoid the need to support multiple overloads by using
templates. As long as the user of the library is calling the function from C++,
this solution works well. However, it is becoming increasingly important to
be able to interface scientific software to other languages (e.g., Python). In
most cases, interfacing is done through a C-like interface, precluding the use
Expand All @@ -66,7 +66,7 @@ Problem Statement
Scientific software developers increasingly need to support multiple FP types.
At the same time, they want their software to be callable from multiple
languages. Unfortunately, this precludes the use of templates at user-facing
interfaces.
interfaces.

*****
Goals
Expand All @@ -76,5 +76,5 @@ Goals
arbitrary FP types.
- Make it easy for users of WTF to compose algorithms with these abstractions.
- Ensure that the user can extend WTF to support their own custom FP types
without needing to modify the WTF source.
- Ensure that the abstractions can be used in a performant manner.
without needing to modify the WTF source.
- Ensure that the abstractions can be used in a performant manner.
2 changes: 1 addition & 1 deletion docs/source/developer/type_erasure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ Type-erasure involves three classes.
3. The "Model" class. This is a class templated on the type of data being
held. It derives from the "Holder" class and implements a model for data of
type ``T``. In WTF these are ``wtf::detail::FloatModel<T>`` and
``wtf::detail::BufferModel<T>``.
``wtf::detail::BufferModel<T>``.
80 changes: 71 additions & 9 deletions include/wtf/buffer/buffer_view.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#pragma once
#include <span>
#include <wtf/buffer/detail_/contiguous_view_model.hpp>
#include <wtf/concepts/float_buffer.hpp>
#include <wtf/concepts/iterator.hpp>
#include <wtf/concepts/wtf_float.hpp>

Expand Down Expand Up @@ -61,6 +62,32 @@ class BufferView {
// Ctors and assignment operators
// -------------------------------------------------------------------------

/** @brief Creates a view of a null buffer.
*
* A null buffer has zero elements and can not be accessed. This ctor is
* used to create a null buffer.
*
* @throw None No-throw guarantee.
*/
BufferView() noexcept : BufferView(nullptr) {}

/** @brief Implicitly converts FloatBuffer to BufferView.
*
* @tparam BufferType2 The type of the buffer being converted from. Must
* satisfy the concept FloatBuffer.
*
* This ctor is used to implicitly convert FloatBuffer objects into
* BufferView objects.
*
* @param[in] buffer The buffer to convert from.
*
* @throw std::bad_alloc if allocating the holder fails. Strong throw
* guarantee.
*/
template<typename BufferType2>
requires(concepts::FloatBuffer<std::remove_reference_t<BufferType2>>)
BufferView(BufferType2&& buffer) : BufferView(buffer.as_view()) {}

/** @brief Creates a BufferView from an existing buffer.
*
* @tparam T The type of floating-point value being held. Must satisfy
Expand All @@ -77,6 +104,9 @@ class BufferView {
explicit BufferView(T* pbuffer, size_type size) :
m_pholder_(std::make_unique<contiguous_model_type<T>>(pbuffer, size)){};

/// Other ctors create the holder and then dispatch to this ctor.
BufferView(holder_pointer pholder) : m_pholder_(std::move(pholder)) {}

/** @brief Conversion from mutable alias to a read-only alias.
*
* @tparam OtherFloat The type of floating-point value being held in
Expand All @@ -94,7 +124,8 @@ class BufferView {
template<concepts::WTFFloat OtherFloat>
requires(is_const && !concepts::ConstQualified<OtherFloat>)
BufferView(const BufferView<OtherFloat>& other) :
m_pholder_(other.m_pholder_->const_clone()) {}
m_pholder_(other.is_holding_() ? other.m_pholder_->const_clone() :
nullptr) {}

/** @brief Creates a new BufferView as a copy of @p other.
*
Expand All @@ -109,7 +140,7 @@ class BufferView {
* guarantee.
*/
BufferView(const BufferView& other) :
m_pholder_(other.m_pholder_->clone()) {}
m_pholder_(other.is_holding_() ? other.m_pholder_->clone() : nullptr) {}

/** @brief Overrides the state of *this with a shallow copy of @p other.
*
Expand All @@ -125,7 +156,12 @@ class BufferView {
* guarantee.
*/
BufferView& operator=(const BufferView& other) {
if(this != &other) { m_pholder_ = other.m_pholder_->clone(); }
if(this != &other) {
if(other.is_holding_())
m_pholder_ = other.m_pholder_->clone();
else
m_pholder_.reset();
}
return *this;
}

Expand Down Expand Up @@ -176,7 +212,10 @@ class BufferView {
* @throw std::out_of_range if @p index is not less than size().
* Strong throw guarantee.
*/
view_type at(size_type index) { return m_pholder_->at(index); }
view_type at(size_type index) {
valid_index_(index);
return m_pholder_->at(index);
}

/** @brief Returns the element with offset @p index.
*
Expand All @@ -191,6 +230,7 @@ class BufferView {
* throw guarantee.
*/
const_view_type at(size_type index) const {
valid_index_(index);
return std::as_const(*m_pholder_).at(index);
}

Expand All @@ -204,7 +244,9 @@ class BufferView {
*
* @throw No-throw guarantee.
*/
size_type size() const noexcept { return m_pholder_->size(); }
size_type size() const noexcept {
return is_holding_() ? m_pholder_->size() : 0;
}

/** @brief Does the held buffer store the values contiguously?
*
Expand All @@ -218,7 +260,9 @@ class BufferView {
*
* @throw No-throw guarantee.
*/
bool is_contiguous() const { return m_pholder_->is_contiguous(); }
bool is_contiguous() const {
return is_holding_() ? m_pholder_->is_contiguous() : true;
}

/** @brief Determines if *this is value equal to @p other.
*
Expand All @@ -233,6 +277,11 @@ class BufferView {
* @throw No-throw guarantee.
*/
bool operator==(const BufferView& other) const {
if(!is_holding_() && !other.is_holding_()) return true;
if(!is_holding_() || !other.is_holding_())
return size() == other.size();

// Both are holding buffers, so compare the buffers
return m_pholder_->are_equal(*other.m_pholder_);
}

Expand All @@ -253,6 +302,11 @@ class BufferView {
template<concepts::WTFFloat OtherFloat>
requires(!std::is_same_v<OtherFloat, FloatType>)
bool operator==(const BufferView<OtherFloat>& other) const {
if(!is_holding_() && !other.is_holding_()) return true;
if(!is_holding_() || !other.is_holding_())
return size() == other.size();

// Both are holding buffers, so compare the buffers
auto plhs = m_pholder_->const_clone();
auto prhs = other.m_pholder_->const_clone();
return plhs->are_equal(*prhs);
Expand Down Expand Up @@ -294,6 +348,7 @@ class BufferView {
*/
template<typename T>
std::span<T> value() {
if(!is_holding_()) return {};
auto& model = downcast_<T>();
return std::span<T>(model.data(), model.size());
}
Expand All @@ -315,14 +370,21 @@ class BufferView {
*/
template<typename T>
std::span<const T> value() const {
if(!is_holding_()) return {};
const auto& model = downcast_<T>();
return std::span<const T>(model.data(), model.size());
}

private:
template<concepts::WTFFloat OtherFloat>
template<concepts::WTFFloat Float2>
friend class BufferView;

void valid_index_(size_type index) const {
if(index >= size()) {
throw std::out_of_range("BufferView: index out of range");
}
}

/// Wraps converting to a contiguous model
template<typename T>
auto& downcast_() {
Expand All @@ -342,8 +404,8 @@ class BufferView {
return const_cast<BufferView*>(this)->downcast_<T>();
}

/// Other ctors create the holder and then dispatch to this ctor.
BufferView(holder_pointer pholder) : m_pholder_(std::move(pholder)) {}
/// True if *this is aliasing a buffer and false otherwise
bool is_holding_() const noexcept { return m_pholder_ != nullptr; }

/// The held type-erased buffer
holder_pointer m_pholder_;
Expand Down
Loading