0

I'm writing an I/O library where the user needs to supply blocks of memory to be read from or written to. Having my library accept a span<T> seems the most natural fit since:

  • It does not impose a container choice on the user. They can use raw pointers, std::vector, or any other container with contiguous storage.
  • It allows me to ensure memory access is safe since I know the size of the buffer.

Unfortunately there are competing implementations of span<T> in Boost, GSL and the standard library (as of C++20). The interface of these implementations are compatible, and from the user perspective it should not matter which one they use.

How can I code my I/O functions so that they work with any of the various implementations of span?

The only approach I can think of at the moment is to include my own implementation of span which would be implicitly constructable from anything with a ::element_type, .data() and .size().

It is important that implicit conversions from containers still be supported so that the user can simply pass a std::vector. For example:

void read_data(span<float> data);

std::vector<float> foo(1024);
read_data(foo);
marack
  • 2,024
  • 22
  • 31
  • 2
    Have a "config.h" to select which one to use is a possibility. – Jarod42 Jun 18 '19 at 23:03
  • Either have a build system or preprocessor flag that picks which one to use or make all of your functions that accept a `span` templates and accept anything `span`-like. – Miles Budnek Jun 18 '19 at 23:15
  • @MilesBudnek: issue with `template` is that there are no longer the conversions to `span` to have identical/expected span interface; I meant `template void read_data(Span span) { span.begin(); /*..*/ }` would not accept `float data[42]` whereas `void read_data(std::span span) { span.begin(); /*..*/ }` would. and forcing user to call explicitly `read_data(std::span(data)` is sad. – Jarod42 Jun 18 '19 at 23:24
  • @Jarod42 Using a `config.h` would mean the choice is made at the library build time. This would prevent different users of the library (on the same system) from using their preferred span implementation. – marack Jun 19 '19 at 00:12
  • @marack Not it it adapts all the different `span`s into one `library::span` that is used internally – Caleth Jun 19 '19 at 10:30
  • @Caleth That's a good point. Maybe the solution is to just pick one that is available on the host system at build time and use it. Then if the user happens to use another implementation they should be implicitly convertible anyway. – marack Jun 19 '19 at 23:52

2 Answers2

1

You might have a config step for user to build your library (or just a config.h for header library only):

Something like:

config.h:

// include guard omitted.

#if defined(SPAN_TYPE) // To allow custom span
    template <typename T> using lib_span = SPAN_TYPE<T>;
#elif defined(USE_STD_SPAN)
    template <typename T> using lib_span = ::std::span<T>;
#elif defined(USE_BOOST_SPAN)
    template <typename T> using lib_span = ::boost::span<T>;
// ...
#else
# error "No span provided"
#endif

And then use lib_span<T> in your code.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

EDIT 3:

I will leave my old posts for anyone who is interested. However, I just found the obvious solution...

#include <user_span>
//dive into your namespace to avoid name collisions
namespace YOUR_LIB
{
    //supply custom type
    template <class T, SIZET size = DEFAULT>
    using span = ::user_span<T, size>;
}
//actually include the library
#include <your_lib>

With this approach you can just use span everywhere and program as usual.

ORIGINAL:

One solution would be to use a template for the span with a default value. Like:

template <template<class,size_t> class Span>
void read_data(Span<float> data);

However, you can run into problems with the various libraries using different signature for their containers. Thus, the better way would be to do:

template <class Span>
void read_data(Span data);

You are missing the type of the values now, but you should be able to retrieve it with:

using T = typename Span::value_type

In addition, you might want to add some std::enable_if (or Concepts) to your functions, which checks if Span actually provides the member functions you are using.

In practice, all of the above can result in very noisy code. If you only have a simple case, the easier way might be to define span with a using declaration of the library user.

The above will probably not work well with the std::vector overloads because the type signatures are somewhat similar. You can solve this by providing explicit std::vector overloads which do the right casts and call the Span version.

EDIT:

From your comment I gather that you want to convert a unspecified type (any container) to a unspecified type (any span). This neither makes sense nor is possible. You will need to define one of the types somewhere.

That being said, you could write conversion independent code and let the user provide the actual conversion. Like:

template <class Span>
void read_data_impl(Span data);

template <class Container>
void read_data(Container data)
{
   read_data_impl(convert(data));
}

The user would then need to supply:

template <class Container>
auto convert(Container& data)
{
    return USERSPAN(data);
}

EDIT 2:

There was a bug in my code. The data in the convert function has to be passed by reference. In addition, it is probably helpful if you pass allow passing a T for the container value by hand. Thus you end up with:

template <class Span>
void read_data_impl(Span data);

template <class Container>
void read_data(Container data)
{
   read_data_impl(convert(data));
}
template <class T, class Container>
void read_data(Container data)
{
   read_data_impl(convert<T>(data));
}

And for conversion:

template <class T, class Container>
auto convert(Container& data)
{
    return convert_impl<T>(data);
}
template <class Container>
auto convert(Container& data)
{
    return convert_impl<typename Container::value_type>(data);
}

The user then has to supply the following function:

template <class T, class Container>
auto convert_impl(Container& data)
{
    return USERSPAN<T>(data);
}
jan.sende
  • 750
  • 6
  • 23
  • Unfortunately this approach does not allow implicit conversion from their container types into the `span`. The user would need to explicitly construct a `span` - which is probably less obvious than the traditional raw pointer and size parameters. – marack Jun 19 '19 at 00:10
  • For this usecase you would provide overloads which do the right thing. Right? – jan.sende Jun 19 '19 at 00:15
  • Providing explicit overloads for containers limits the user to a certain set of containers. The point of passing data by `span` is that it lets the user safely pass data without the library needing to know what actual container is. It could be a custom container that only exists in the user's code that the library could never have knowledge of. – marack Jun 19 '19 at 00:51
  • Whereas `convert(data)` might work with class type (thanks to ADL), it won't for built-in type a c-array:[Demo](http://coliru.stacked-crooked.com/a/62affc50f8f6b2b9) – Jarod42 Jun 20 '19 at 11:15
  • @Jarod42 First of all, he didn't say that c-arrays are expected. Secondly, with my approach this is the responsibility of the user, and lastly you have to either declare the convert function before the call or you have to put it up top. [Demo](http://coliru.stacked-crooked.com/a/c162aa0911135a91) Plus, thanks for finding my bug! The convert function needs to capture by reference... – jan.sende Jun 20 '19 at 15:10
  • OP expects `span` (and c-array is in *"any other container with contiguous storage"*), and span has constructor for `c-array`. `read_data` is from the library, so user cannot (reasonably) provide code before that (`#include` in middle of the code is ugly... requiring some `#include` order is to avoid). Don't hesitate to improve answer by fixing bug. and *"capture"* is used for lambda, not for function parameter, it would be *"pass by (const) reference"* against your current *"pass by value"*. – Jarod42 Jun 20 '19 at 15:50