I would like to create a concept
which allows me to define a single function for each read
and write
from/to a data container. The data container should have a .data()
and a .size()
methods, so that the size can be checked before reading/writing.
Additionally, for the writing function, the datatype should be marked const
, so that I cannot change the variable which is pointing to, or its content by mistake.
As example I would like to support datatypes like std::vector
and if possible std::span
if I only have the pointer and the size separated.
To check if a container has both methods, I came up with
template <typename T>
concept HasDataAndSize =
requires(T t) { t.size(); } && requires(T t) { t.data(); } &&
requires(T t) { std::is_same<decltype(t.size()), std::size_t>::value; } &&
requires(T t) { std::is_pointer<decltype(t.data())>::value; };
The problem is, if I create a function
template<HasDataAndSize T> void write(const T& data)
the data is writable for a std::span
object.
Additionally, if I create a reading function
template<HasDataAndSize T> void read(T read)
std::span
works, however containers like std::vector
are copied and the change accessible outside the function. If I use a T &
std::vector
works, but I cannot use std::span<>
from a temporary like read(std::span{data_ptr, data_size})
Currently I think I only can allow containers, which do not allow changing the ptr inside it, like std::span
can. However, my concept still lets std::span
through, and I don't know how to stop it.
Or is there a better solution? / reason why I should use another design?
I created a small example to play around with: https://godbolt.org/z/nT7jWve91
To clarify, I am writing on a library which, among other things, has to write data from different data containers into a file or read the content from the file into the data.
Let's start with this:
template <typename T>
concept HasDataAndSize =
requires(T t) { t.size(); } && requires(T t) { t.data(); } &&
requires(T t) { std::is_same<decltype(t.size()), std::size_t>::value; } &&
requires(T t) { std::is_pointer<decltype(t.data())>::value; };
First and foremost, there is no need to separate everything into its own requires-expression, this adds a lot of syntax with no benefit. This is equivalent to:
template <typename T>
concept HasDataAndSize =
requires(T t) {
t.size();
t.data();
std::is_same<decltype(t.size()), std::size_t>::value; // (1)
std::is_pointer<decltype(t.data())>::value; // (2)
};
Next, the lines marked (1)
and (2)
above don't... actually do what you think they do. Indeed, they don't do anything at all. What you're checking is that the expression std::is_same<T, U>::value
is a valid expression - which of course it is, regardless of T
and U
. You're not actually checking that the types are the same.
In order to do that, you need to either prefix the expression with requires
or use the compound requirement syntax:
template <typename T>
concept HasDataAndSize =
requires(T t) {
// t.size() needs to be size_t
{ t.size() } -> std::same_as<size_t>;
// t.data() needs to be valid and have pointer type
t.data();
requires std::is_pointer_v<decltype(t.data())>;
};
Once we get there, the you have this other problem
template<HasDataAndSize T> void write(const T& data)
This is checking that T
has data()
and size()
, but that's not really the relevant question here, since data
isn't a T
, it's a T const
. Here, you need to do:
template <class T>
requires HasDataAndSize<T const>
void write(const T& data)
or introduce a helper concept:
template <class T> concept HasConstDataAndSize = HasDataAndSize<T const>;
template<HasConstDataAndSize T> void write(const T& data)
or use a forwarding reference (which will ensure that T
is deduced in a way that's useful for concepts):
template<HasDataAndSize T> void write(T&& data)
Lastly, this is more of an API issue, but it's probably better to eschew this entirely and just write:
void write(std::span<std::byte> );
void read(std::span<std::byte const> );
Although generally I don't really understand what the API here is supposed to do.