There was a special case for ArrayHandleMultiplexer where if you gave it just one type it would treat that as a value type rather than an array to support and instead provide a default list of types. However, GCC 4.8 is having trouble compiling the code to create the default list, the semantics are confusing, and the more I think about it the less likely I think we will need this functionality. So, just getting rid of that.
8.2 KiB
Add ArrayHandleMultiplexer
vtkm::cont::ArrayHandleMultiplexer
is a fancy ArrayHandle
that can
mimic being any one of a list of other ArrayHandle
s. When declared, a set
of a list of ArrayHandle
s is given to ArrayHandleMultiplexer
. To use
the ArrayHandleMultiplexer
it is set to an instance of one of these other
ArrayHandle
s. Thus, once you compile code to use an
ArrayHandleMultiplexer
, you can at runtime select any of the types it
supports.
The intention is convert the data from a vtkm::cont::VariantArrayHandle
to a vtkm::cont::ArrayHandleMultiplexer
of some known types. The
ArrayHandleMultiplexer
can be compiled statically (that is, no virtual
methods are needed). Although the compiler must implement all possible
implementations of the multiplexer, two or more ArrayHandleMultiplexer
s
can be used together without having to compile every possible combination
of all of them.
Motivation
ArrayHandle
is a very flexible templated class that allows us to use the
compiler to adapt our code to pretty much any type of memory layout or
on-line processing. Unfortunately, the template approach requires the code
to know the exact type during compile time.
That is a problem when retrieving data from a
vtkm::cont::VariantArrayHandle
, which is the case, for example, when
getting data from a vtkm::cont::DataSet
. The actual type of the array
stored in a vtkm::cont::VariantArrayHandle
is generally not known at
compile time at the code location where the data is pulled.
Our first approach to this problem was to use metatemplate programming to
iterate over all possible types in the VariantArrayHandle
. Although this
works, it means that if two or more VariantArrayHandle
s are dispatched in
a function call, the compiler needs to generate all possible combinations
of the two. This causes long compile times and large executable sizes. It
has lead us to limit the number of types we support, which causes problems
with unsupported arrays.
Our second approach to this problem was to create ArrayHandleVirtual
to
hide the array type behind a virtual method. This works very well, but is
causing us significant problems on accelerators. Although virtual methods
are supported by CUDA, there are numerous problems that can come up with
the compiled code (such as unknown stack depths or virtual methods
extending across libraries). It is also unknown what problems we will
encounter with other accelerator architectures.
ArrayHandleMultiplexer
is meant to be a compromise between these two
approaches. Although we are still using metatemplate programming tricks to
iterate over multiple implementations, this compiler looping is localized
to the code to lookup values in the array. This, it is a small amount of
code that needs to be created for each version supported by the
ArrayHandle
. Also, the code paths can be created independently for each
ArrayHandleMultiplexer
. Thus, you do not get into the problem of a
combinatorial explosion of types that need to be addressed.
Although ArrayHandleMultiplexer
still has the problem of being unable to
store a type that is not explicitly listed, the localized expression should
allow us to support many types. By default, we are adding lots of
ArrayHandleCast
s to the list of supported types. The intention of this is
to allow a filter to specify a value type it operates on and then cast
everything to that type. This further allows us to reduce combination of
types that we have to support.
Use
The ArrayHandleMultiplexer
templated class takes a variable number of
template parameters. All the template parameters are expected to be types
of ArrayHandle
s that the ArrayHandleMultiplexer
can assume.
For example, let's say we have a use case where we need an array of
indices. Normally, the indices are sequential (0, 1, 2,...), but sometimes
we need to define a custom set of indices. When the indices are sequential,
then an ArrayHandleIndex
is the best representation. Normally if you also
need to support general arrays you would first have to deep copy the
indices into a physical array. However, with an ArrayHandleMultiplexer
you can support both.
vtkm::cont::ArrayHandleMultiplexer<vtkm::cont::ArrayHandleIndex,
vtkm::cont::ArrayHandle<vtkm::Id>> indices;
indices = vtkm::cont::ArrayHandleIndex(ARRAY_SIZE);
indices
can now be used like any other ArrayHandle
, but for now is
behaving like an ArrayHandleIndex
. That is, it takes (almost) no actual
space. But if you need to use explicit indices, you can set the indices
array to an actual array of indices
vtkm::cont::ArrayHandle<vtkm::Id> indicesInMemory;
// Fill indicesInMemory...
indices = indicesInMemory;
All the code that uses indices
will continue to work.
Variant
To implement ArrayHandleMultiplexer
, the class vtkm::internal::Variant
was introduced. Although this is an internal class that is not exposed
through the array handle, it is worth documenting its addition as it will
be useful to implement other multiplexing type of objects (such as for
cell sets and locators).
vtkm::internal::Variant
is a simplified version of C++17's std::variant
or boost's variant
. One of the significant differences between VTK-m's
Variant
and these other versions is that VTK-m's version does not throw
exceptions on error. Instead, behavior becomes undefined. This is
intentional as not all platforms support exceptions and there could be
consequences on just the possibility for those that do.
Like the aforementioned classes that vtkm::internal::Variant
is based on,
it behaves much like a union
of a set of types. Those types are listed as
the Variant
's template parameters. The Variant
can be set to any one of
these types either through construction or assignment. You can also use the
Emplace
method to construct the object in a Variant
.
vtkm::internal::Variant<int, float, std::string> variant(5);
// variant is now an int.
variant = 5.0f;
// variant is now a float.
variant.Emplace<std::string>("Hello world");
// variant is now an std::string.
The Variant
maintains the index of which type it is holding. It has
several helpful items to manage the type and index of contained objects:
GetIndex()
: A method to retrieve the template parameter index of the type currently held. In the previous example, the index starts at 0, becomes 1, then becomes 2.GetIndexOf<T>()
: A static method that returns aconstexpr
of the index of a given type. In the previous example,variant.GetIndexOf<float>()
would return 1.Get<T or I>()
: Given a type, returns the contained object as that type. Given a number, returns the contained object as a type of the corresponding index. In the previous example, eithervariant.Get<1>()
orvariant.Get<float>()
would return thefloat
value. The behavior is undefined if the object is not the requested type.IsValid()
: A method that can be used to determine whether theVariant
holds an object that can be operated on.Reset()
: A method to remove any contained object and restore to an invalid state.
Finally, Variant
contains a CastAndCall
method. This method takes a
functor followed by a list of optional arguments. The contained object is
cast to the appropriate type and the functor is called with the cast object
followed by the provided arguments. If the functor returns a value, that
value is returned by CastAndCall
.
CastAndCall
is an important functionality that makes it easy to wrap
multiplexer objects around a Variant
. For example, here is how you could
implement executing the Value
method in an implicit function multiplexer.
class ImplicitFunctionMultiplexer
{
vtkm::internal::Variant<Box, Plane, Sphere> ImplicitFunctionVariant;
// ...
struct ValueFunctor
{
template <typename ImplicitFunctionType>
vtkm::FloatDefault operator()(const ImplicitFunctionType& implicitFunction,
const vtkm::Vec<vtkm::FloatDefault, 3>& point)
{
return implicitFunction.Value(point);
}
};
vtkm::FloatDefault Value(const vtkm::Vec<vtkm::FloatDefault, 3>& point) const
{
return this->ImplicitFunctionVariant.CastAndCall(ValueFunctor{}, point);
}