Commit 15d1fbb1 authored by Tobias Lasser's avatar Tobias Lasser

Add DataHandlerMapCPU, move copy-on-write functionality to DataHandler, add...

Add DataHandlerMapCPU, move copy-on-write functionality to DataHandler, add block functionality to DataContainer
parent bf27c029
Pipeline #186717 passed with stages
in 18 minutes and 42 seconds
......@@ -33,12 +33,12 @@ namespace elsa
index_t BlockDescriptor::getNumberOfBlocks() const { return _blockDescriptors.size(); }
const DataDescriptor& BlockDescriptor::getIthDescriptor(index_t i) const
const DataDescriptor& BlockDescriptor::getDescriptorOfBlock(index_t i) const
{
return *_blockDescriptors.at(i);
}
index_t BlockDescriptor::getIthBlockOffset(elsa::index_t i) const
index_t BlockDescriptor::getOffsetOfBlock(elsa::index_t i) const
{
if (i < 0 || i >= _blockOffsets.size())
throw std::invalid_argument("BlockDescriptor: index i is out of bounds");
......
......@@ -50,10 +50,10 @@ namespace elsa
index_t getNumberOfBlocks() const;
/// return the DataDescriptor of the i-th block
const DataDescriptor& getIthDescriptor(index_t i) const;
const DataDescriptor& getDescriptorOfBlock(index_t i) const;
/// return the offset to access the data of the i-th block
index_t getIthBlockOffset(index_t i) const;
index_t getOffsetOfBlock(index_t i) const;
protected:
/// vector of DataDescriptors describing the individual blocks
......
......@@ -16,6 +16,7 @@ set(MODULE_HEADERS
DataContainerIterator.h
DataHandler.h
DataHandlerCPU.h
DataHandlerMapCPU.h
LinearOperator.h)
# list all the code files of the module
......@@ -24,6 +25,7 @@ set(MODULE_SOURCES
BlockDescriptor.cpp
DataContainer.cpp
DataHandlerCPU.cpp
DataHandlerMapCPU.cpp
LinearOperator.cpp)
......
......@@ -27,10 +27,7 @@ namespace elsa
std::unique_ptr<Derived> clone() const { return std::unique_ptr<Derived>(cloneImpl()); }
/// comparison operators
bool operator==(const Derived& other) const
{
return typeid(*this) == typeid(other) && isEqual(other);
}
bool operator==(const Derived& other) const { return isEqual(other); }
bool operator!=(const Derived& other) const { return !(*this == other); }
......
#include "DataContainer.h"
#include "DataHandlerCPU.h"
#include "DataHandlerMapCPU.h"
#include "BlockDescriptor.h"
#include <stdexcept>
#include <utility>
......@@ -31,7 +33,7 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>::DataContainer(const DataContainer<data_t>& other)
: _dataDescriptor{other._dataDescriptor->clone()}, _dataHandler{other._dataHandler}
: _dataDescriptor{other._dataDescriptor->clone()}, _dataHandler{other._dataHandler->clone()}
{
}
......@@ -40,7 +42,12 @@ namespace elsa
{
if (this != &other) {
_dataDescriptor = other._dataDescriptor->clone();
_dataHandler = other._dataHandler;
if (_dataHandler) {
*_dataHandler = *other._dataHandler;
} else {
_dataHandler = other._dataHandler->clone();
}
}
return *this;
......@@ -57,10 +64,14 @@ namespace elsa
}
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator=(DataContainer<data_t>&& other) noexcept
DataContainer<data_t>& DataContainer<data_t>::operator=(DataContainer<data_t>&& other)
{
_dataDescriptor = std::move(other._dataDescriptor);
_dataHandler = std::move(other._dataHandler);
if (_dataHandler) {
*_dataHandler = std::move(*other._dataHandler);
} else {
_dataHandler = std::move(other._dataHandler);
}
// leave other in a valid state
other._dataDescriptor = nullptr;
......@@ -84,27 +95,26 @@ namespace elsa
template <typename data_t>
data_t& DataContainer<data_t>::operator[](index_t index)
{
detach();
return (*_dataHandler)[index];
}
template <typename data_t>
const data_t& DataContainer<data_t>::operator[](index_t index) const
{
return (*_dataHandler)[index];
return static_cast<const DataHandler<data_t>&>(*_dataHandler)[index];
}
template <typename data_t>
data_t& DataContainer<data_t>::operator()(IndexVector_t coordinate)
{
detach();
return (*_dataHandler)[_dataDescriptor->getIndexFromCoordinate(coordinate)];
}
template <typename data_t>
const data_t& DataContainer<data_t>::operator()(IndexVector_t coordinate) const
{
return (*_dataHandler)[_dataDescriptor->getIndexFromCoordinate(coordinate)];
return static_cast<const DataHandler<data_t>&>(
*_dataHandler)[_dataDescriptor->getIndexFromCoordinate(coordinate)];
}
template <typename data_t>
......@@ -164,7 +174,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator+=(const DataContainer<data_t>& dc)
{
detach();
*_dataHandler += *dc._dataHandler;
return *this;
}
......@@ -172,7 +181,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator-=(const DataContainer<data_t>& dc)
{
detach();
*_dataHandler -= *dc._dataHandler;
return *this;
}
......@@ -180,7 +188,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator*=(const DataContainer<data_t>& dc)
{
detach();
*_dataHandler *= *dc._dataHandler;
return *this;
}
......@@ -188,7 +195,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator/=(const DataContainer<data_t>& dc)
{
detach();
*_dataHandler /= *dc._dataHandler;
return *this;
}
......@@ -196,7 +202,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator+=(data_t scalar)
{
detach();
*_dataHandler += scalar;
return *this;
}
......@@ -204,7 +209,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator-=(data_t scalar)
{
detach();
*_dataHandler -= scalar;
return *this;
}
......@@ -212,7 +216,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator*=(data_t scalar)
{
detach();
*_dataHandler *= scalar;
return *this;
}
......@@ -220,7 +223,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator/=(data_t scalar)
{
detach();
*_dataHandler /= scalar;
return *this;
}
......@@ -228,7 +230,6 @@ namespace elsa
template <typename data_t>
DataContainer<data_t>& DataContainer<data_t>::operator=(data_t scalar)
{
detach();
*_dataHandler = scalar;
return *this;
}
......@@ -272,19 +273,67 @@ namespace elsa
}
template <typename data_t>
void DataContainer<data_t>::detach()
DataContainer<data_t> DataContainer<data_t>::getBlock(index_t i)
{
if (_dataHandler.use_count() != 1) {
#pragma omp barrier
#pragma omp single
_dataHandler = _dataHandler->clone();
}
const auto blockDesc = dynamic_cast<const BlockDescriptor*>(_dataDescriptor.get());
if (!blockDesc)
throw std::logic_error("DataContainer: cannot get block from not-blocked container");
if (i >= blockDesc->getNumberOfBlocks() || i < 0)
throw std::invalid_argument("DataContainer: block index out of bounds");
index_t startIndex = blockDesc->getOffsetOfBlock(i);
const auto& ithDesc = blockDesc->getDescriptorOfBlock(i);
index_t blockSize = ithDesc.getNumberOfCoefficients();
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize)};
}
template <typename data_t>
const DataContainer<data_t> DataContainer<data_t>::getBlock(index_t i) const
{
const auto blockDesc = dynamic_cast<const BlockDescriptor*>(_dataDescriptor.get());
if (!blockDesc)
throw std::logic_error("DataContainer: cannot get block from not-blocked container");
if (i >= blockDesc->getNumberOfBlocks() || i < 0)
throw std::invalid_argument("DataContainer: block index out of bounds");
index_t startIndex = blockDesc->getOffsetOfBlock(i);
const auto& ithDesc = blockDesc->getDescriptorOfBlock(i);
index_t blockSize = ithDesc.getNumberOfCoefficients();
// getBlock() returns a pointer to non-const DH, but that's fine as it gets wrapped in a
// constant container
return DataContainer<data_t>{ithDesc, _dataHandler->getBlock(startIndex, blockSize)};
}
template <typename data_t>
DataContainer<data_t> DataContainer<data_t>::viewAs(const DataDescriptor& dataDescriptor)
{
if (dataDescriptor.getNumberOfCoefficients() != getSize())
throw std::invalid_argument("DataContainer: view must have same size as container");
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize())};
}
template <typename data_t>
const DataContainer<data_t>
DataContainer<data_t>::viewAs(const DataDescriptor& dataDescriptor) const
{
if (dataDescriptor.getNumberOfCoefficients() != getSize())
throw std::invalid_argument("DataContainer: view must have same size as container");
// getBlock() returns a pointer to non-const DH, but that's fine as it gets wrapped in a
// constant container
return DataContainer<data_t>{dataDescriptor, _dataHandler->getBlock(0, getSize())};
}
template <typename data_t>
typename DataContainer<data_t>::iterator DataContainer<data_t>::begin()
{
detach();
return iterator(&(*this)[0]);
}
......@@ -303,7 +352,6 @@ namespace elsa
template <typename data_t>
typename DataContainer<data_t>::iterator DataContainer<data_t>::end()
{
detach();
return iterator(&(*this)[0] + getSize());
}
......@@ -322,7 +370,6 @@ namespace elsa
template <typename data_t>
typename DataContainer<data_t>::reverse_iterator DataContainer<data_t>::rbegin()
{
detach();
return reverse_iterator(end());
}
......@@ -341,7 +388,6 @@ namespace elsa
template <typename data_t>
typename DataContainer<data_t>::reverse_iterator DataContainer<data_t>::rend()
{
detach();
return reverse_iterator(begin());
}
......
......@@ -10,12 +10,6 @@
namespace elsa
{
// forward declaration for friend test function
template <typename data_t = real_t>
class DataContainer;
// used for testing and defined in test file (declared as friend)
template <typename data_t>
int useCount(const DataContainer<data_t>& /*dc*/);
/**
* \brief class representing and storing a linearized n-dimensional signal
......@@ -23,17 +17,15 @@ namespace elsa
* \author Matthias Wieczorek - initial code
* \author Tobias Lasser - rewrite, modularization, modernization
* \author David Frank - added DataHandler concept, iterators
* \author Nikola Dinev - add block support
*
* \tparam data_t - data type that is stored in the DataContainer, defaulting to real_t.
*
* This class provides a container for a signal that is stored in memory. This signal can
* be n-dimensional, and will be stored in memory in a linearized fashion. The information
* on how this linearization is performed is provided by an associated DataDescriptor.
*
* The class implements copy-on-write. Therefore any non-const functions should call the
* detach() function first to trigger the copy-on-write mechanism.
*/
template <typename data_t>
template <typename data_t = real_t>
class DataContainer
{
public:
......@@ -42,8 +34,6 @@ namespace elsa
*
* The following handler types are currently supported:
* - CPU: data is stored as an Eigen::Matrix in CPU main memory
* - MAP: data is not explicitly stored, but using an Eigen::Map to refer to other
* storage
*/
enum class DataHandlerType { CPU };
......@@ -104,7 +94,7 @@ namespace elsa
* fulfilled for any member functions, the object should not be used. After move- or copy-
* assignment, this is possible again.
*/
DataContainer<data_t>& operator=(DataContainer<data_t>&& other) noexcept;
DataContainer<data_t>& operator=(DataContainer<data_t>&& other);
/// return the current DataDescriptor
const DataDescriptor& getDataDescriptor() const;
......@@ -207,8 +197,17 @@ namespace elsa
/// comparison with another DataContainer
bool operator!=(const DataContainer<data_t>& other) const;
/// used for testing only and defined in test file
friend int useCount<>(const DataContainer<data_t>& dc);
/// returns a reference to the i-th block, wrapped in a DataContainer
DataContainer<data_t> getBlock(index_t i);
/// returns a const reference to the i-th block, wrapped in a DataContainer
const DataContainer<data_t> getBlock(index_t i) const;
/// return a view of this DataContainer with a different descriptor
DataContainer<data_t> viewAs(const DataDescriptor& dataDescriptor);
/// return a const view of this DataContainer with a different descriptor
const DataContainer<data_t> viewAs(const DataDescriptor& dataDescriptor) const;
/// iterator for DataContainer (random access and continuous)
using iterator = DataContainerIterator<DataContainer<data_t>>;
......@@ -280,7 +279,7 @@ namespace elsa
/// the current DataDescriptor
std::unique_ptr<DataDescriptor> _dataDescriptor;
/// the current DataHandler
std::shared_ptr<DataHandler<data_t>> _dataHandler;
std::unique_ptr<DataHandler<data_t>> _dataHandler;
/// factory method to create DataHandlers based on handlerType with perfect forwarding of
/// constructor arguments
......@@ -291,9 +290,6 @@ namespace elsa
/// private constructor accepting a DataDescriptor and a DataHandler
explicit DataContainer(const DataDescriptor& dataDescriptor,
std::unique_ptr<DataHandler<data_t>> dataHandler);
/// creates the deep copy for the copy-on-write mechanism
void detach();
};
/// element-wise addition of two DataContainers
......
......@@ -12,6 +12,7 @@ namespace elsa
*
* \author David Frank - initial code
* \author Tobias Lasser - modularization, modernization
* \author Nikola Dinev - add block support
*
* This abstract base class serves as an interface for data handlers, which encapsulate the
* actual data being stored e.g. in main memory of the CPU or in various memory types of GPUs.
......@@ -89,6 +90,36 @@ namespace elsa
/// assign a scalar to all elements of the data vector
virtual DataHandler<data_t>& operator=(data_t scalar) = 0;
/// copy assignment operator
DataHandler<data_t>& operator=(const DataHandler<data_t>& other)
{
if (other.getSize() != getSize())
throw std::invalid_argument("DataHandler: assignment argument has wrong size");
assign(other);
return *this;
}
/// move assignment operator
DataHandler<data_t>& operator=(DataHandler<data_t>&& other)
{
if (other.getSize() != getSize())
throw std::invalid_argument("DataHandler: assignment argument has wrong size");
assign(std::move(other));
return *this;
}
/// return a reference to the sequential block starting at startIndex and containing
/// numberOfElements elements
virtual std::unique_ptr<DataHandler<data_t>> getBlock(index_t startIndex,
index_t numberOfElements) = 0;
/// return a const reference to the sequential block starting at startIndex and containing
/// numberOfElements elements
virtual std::unique_ptr<const DataHandler<data_t>>
getBlock(index_t startIndex, index_t numberOfElements) const = 0;
protected:
/// slow element-wise dot product fall-back for when DataHandler types do not match
data_t slowDotProduct(const DataHandler<data_t>& v) const
......@@ -126,6 +157,19 @@ namespace elsa
for (index_t i = 0; i < getSize(); ++i)
(*this)[i] /= v[i];
}
/// slow element-wise assignment fall-back for when DataHandler types do not match
void slowAssign(const DataHandler<data_t>& other)
{
for (index_t i = 0; i < getSize(); ++i)
(*this)[i] = other[i];
}
/// derived classes should override this method to implement copy assignment
virtual void assign(const DataHandler<data_t>& other) = 0;
/// derived classes should override this method to implement move assignment
virtual void assign(DataHandler<data_t>&& other) = 0;
};
/// element-wise addition of two DataHandlers
......
This diff is collapsed.
......@@ -5,8 +5,20 @@
#include <Eigen/Core>
#include <list>
namespace elsa
{
// forward declaration, allows mutual friending
template <typename data_t>
class DataHandlerMapCPU;
// forward declaration for friend test function
template <typename data_t = real_t>
class DataHandlerCPU;
// forward declaration, used for testing and defined in test file (declared as friend)
template <typename data_t>
int useCount(const DataHandlerCPU<data_t>&);
/**
* \brief Class representing and owning a vector stored in CPU main memory (using
......@@ -16,10 +28,23 @@ namespace elsa
*
* \author David Frank - main code
* \author Tobias Lasser - modularization and modernization
* \author Nikola Dinev - integration of map and copy-on-write concepts
*
* The class implements copy-on-write. Therefore any non-const functions should call the
* detach() function first to trigger the copy-on-write mechanism.
*
* DataHandlerCPU and DataHandlerMapCPU are mutual friend classes allowing for the vectorization
* of arithmetic operations with the help of Eigen. A strong bidirectional link exists
* between the two classes. A Map is associated with the DataHandlerCPU from which it was
* created for the entirety of its lifetime. If the DataHandlerCPU starts managing a new vector
* (e.g. through a call to detach()), all associated Maps will also be updated.
*/
template <typename data_t = real_t>
template <typename data_t>
class DataHandlerCPU : public DataHandler<data_t>
{
/// declare DataHandlerMapCPU as friend, allows the use of Eigen for improved performance
friend DataHandlerMapCPU<data_t>;
protected:
/// convenience typedef for the Eigen::Matrix data vector
using DataVector_t = Eigen::Matrix<data_t, Eigen::Dynamic, 1>;
......@@ -29,7 +54,7 @@ namespace elsa
DataHandlerCPU() = delete;
/// default destructor
~DataHandlerCPU() override = default;
~DataHandlerCPU() override;
/**
* \brief Constructor initializing an appropriately sized vector with zeros
......@@ -49,6 +74,12 @@ namespace elsa
*/
explicit DataHandlerCPU(DataVector_t vector);
/// copy constructor
DataHandlerCPU(const DataHandlerCPU<data_t>& other);
/// move constructor
DataHandlerCPU(DataHandlerCPU<data_t>&& other);
/// return the size of the vector
index_t getSize() const override;
......@@ -85,6 +116,15 @@ namespace elsa
/// return a new DataHandler with element-wise logarithms of this one
std::unique_ptr<DataHandler<data_t>> log() const override;
/// copy assign another DataHandlerCPU to this, other types handled in assign()
DataHandlerCPU<data_t>& operator=(const DataHandlerCPU<data_t>& v);
/// move assign another DataHandlerCPU to this, other types handled in assign()
DataHandlerCPU<data_t>& operator=(DataHandlerCPU<data_t>&& v);
/// lift copy and move assignment operators from base class
using DataHandler<data_t>::operator=;
/// compute in-place element-wise addition of another vector v
DataHandler<data_t>& operator+=(const DataHandler<data_t>& v) override;
......@@ -112,15 +152,51 @@ namespace elsa
/// assign a scalar to all elements of the data vector
DataHandler<data_t>& operator=(data_t scalar) override;
/// return a reference to the sequential block starting at startIndex and containing
/// numberOfElements elements
std::unique_ptr<DataHandler<data_t>> getBlock(index_t startIndex,
index_t numberOfElements) override;
/// return a const reference to the sequential block starting at startIndex and containing
/// numberOfElements elements
std::unique_ptr<const DataHandler<data_t>>
getBlock(index_t startIndex, index_t numberOfElements) const override;
/// used for testing only and defined in test file
friend int useCount<>(const DataHandlerCPU<data_t>& dh);
protected:
/// the vector storing the data
DataVector_t _data;
std::shared_ptr<DataVector_t> _data;
/// list of DataHandlerMaps referring to blocks of this
std::list<DataHandlerMapCPU<data_t>*> _associatedMaps;
/// implement the polymorphic clone operation
DataHandlerCPU<data_t>* cloneImpl() const override;
/// implement the polymorphic comparison operation
bool isEqual(const DataHandler<data_t>& other) const override;
/// copy the data stored in other
void assign(const DataHandler<data_t>& other) override;
/// move the data stored in other if other is of the same type, otherwise copy the data
void assign(DataHandler<data_t>&& other) override;
private:
/// creates the deep copy for the copy-on-write mechanism
void detach();
/// same as detach() but leaving an uninitialized block of numberOfElements elements
/// starting at index startIndex
void detachWithUninitializedBlock(index_t startIndex, index_t numberOfElements);
/// change the vector being handled
void attach(const std::shared_ptr<DataVector_t>& data);
/// change the vector being handled (rvalue version)
void attach(std::shared_ptr<DataVector_t>&& data);
};
} // namespace elsa
This diff is collapsed.
#pragma once
#include "elsa.h"
#include "DataHandler.h"
#include <Eigen/Core>
#include <list>
namespace elsa
{
/// forward declaration, allows mutual friending
template <typename data_t>
class DataHandlerCPU;
/**
* \brief Class referencing a vector stored in CPU main memory, or a part thereof (using
* Eigen::Map)
*
* \tparam data_t data type of vector
*
* \author David Frank - main code
* \author Tobias Lasser - modularization, fixes
* \author Nikola Dinev - integration with the copy-on-write mechanism
*
* This class does not own or manage its own memory. It is bound to a DataHandlerCPU (the data
* owner) at its creation, and serves as a reference to a sequential block of memory owned by
* the DataHandlerCPU. As such, changes to the Map will affect the DataHandlerCPU and vice
* versa.
*
* Maps do not support move assignment, and remain bound to the original data owner until
* destructed.
*
* Maps provide only limited support for copy-on-write. Unless the Map is referencing the
* entirety of the vector managed by the data owner, assigning to the Map or cloning will always
* trigger a deep copy.
*
* Cloning a Map produces a new DataHandlerCPU, managing a new chunk of memory. The contents of
* the memory are equivalent to the contents of the block referenced by the Map, but the two are
* not associated.
*/
template <typename data_t = real_t>
class DataHandlerMapCPU : public DataHandler<data_t>
{
/// declare DataHandlerCPU as friend, allows the use of Eigen for improved performance
friend class DataHandlerCPU<data_t>;
protected:
/// convenience typedef for the Eigen::Matrix data vector
using DataVector_t = Eigen::Matrix<data_t, Eigen::Dynamic, 1>;
/// convenience typedef for the Eigen::Map
using DataMap_t = Eigen::Map<DataVector_t>;
public:
/// copy constructor
DataHandlerMapCPU(const DataHandlerMapCPU<data_t>& other);
/// default move constructor
DataHandlerMapCPU(DataHandlerMapCPU<data_t>&& other) = default;
/// default destructor
~DataHandlerMapCPU() override;
/// return the size of the vector
index_t getSize() const override;
/// return the index-th element of the data vector (not bounds checked!)
data_t& operator[](index_t index) override;
/// return the index-th element of the data vector as read-only (not bound checked!)
const data_t& operator[](index_t index) const override;
/// return the dot product of the data vector with vector v
data_t dot(const DataHandler<data_t>& v) const override;
/// return the squared l2 norm of the data vector (dot product with itself)
data_t squaredL2Norm() const override;
/// return the l1 norm of the data vector (sum of absolute values)
data_t l1Norm() const override;
/// return the linf norm of the data vector (maximum of absolute values)
data_t lInfNorm() const override;
/// return the sum of all elements of the data vector
data_t sum() const override;
/// return a new DataHandler with element-wise squared values of this one
std::unique_ptr<DataHandler<data_t>> square() const override;
/// return a new DataHandler with element-wise square roots of this one
std::unique_ptr<DataHandler<data_t>> sqrt() const override;
/// return a new DataHandler with element-wise exponentials of this one
std::unique_ptr<DataHandler<data_t>> exp() const override;
/// return a new DataHandler with element-wise logarithms of this one
std::unique_ptr<DataHandler<data_t>> log() const override;
/// compute in-place element-wise addition of another vector v
DataHandler<data_t>& operator+=(const DataHandler<data_t>& v) override;
/// compute in-place element-wise subtraction of another vector v
DataHandler<data_t>& operator-=(const DataHandler<data_t>& v) override;
/// compute in-place element-wise multiplication by another vector v
DataHandler<data_t>& operator*=(const DataHandler<data_t>& v) override;
/// compute in-place element-wise division by another vector v
DataHandler<data_t>& operator/=(const DataHandler<data_t>& v) override;
/// copy assign another DataHandlerMapCPU to this, other types handled in assign()
DataHandlerMapCPU<data_t>& operator=(const DataHandlerMapCPU<data_t>& v);
DataHandlerMapCPU<data_t>& operator=(DataHandlerMapCPU<data_t>&&) = default;
/// lift copy and move assignment operators from base class
using DataHandler<data_t>::operator=;
/// compute in-place addition of a scalar
DataHandler<data_t>& operator+=(data_t scalar) override;
/// compute in-place subtraction of a scalar
DataHandler<data_t>& operator-=(data_t scalar) override;
/// compute in-place multiplication by a scalar
DataHandler<data_t>& operator*=(data_t scalar) override;