Reputation: 147
I have a variadic template function that takes as parameters file name, delimiter, and non-specified number of containers as columns. This function then parses to file all values in containers that represent columns and between all values put delimiter.
bool parseToFile (const std::string& file, const char delimiter, auto&... columns)
{
bool done {false};
std::ofstream out;
out.open (file, std::ios::out | std::ios::trunc);
if (!out)
{
done = false;
std::cout << "SaveFile: Cannot open file: " << file << "!" ;
std::cout << std::strerror (errno) << std::endl;
}
else
{
std::size_t maxContainerSize {0};
auto maxSize = [&maxContainerSize] (std::size_t containerSize)
{
maxContainerSize = std::max (maxContainerSize, containerSize);
};
(maxSize (columns.size()), ...);
std::size_t i {0};
auto implementation = [&i, delimiter, &out] (auto & cont, std::size_t s)
{
if (i < cont.size())
out << cont[i];
out << delimiter;
};
for (; i < maxContainerSize; ++i)
{
(implementation (columns, columns.size()), ...);
out << "\n";
}
out << std::flush;
done = true;
return done;
}
This works as intended correctly.
But this template has several constraints releating to what can be parameter pack "columns"
How should I change my code to implement this constraint?
I have tried this:
template <template <typename, typename> class Container,
typename Value,
typename Allocator = std::allocator<Value>>
concept containerHasPrintable = requires (std::ostream& out, Container<Value,Allocator>& data)
{
out << data[0];
};
// and definition:
bool parseToFile (const std::string& file, const char delimiter, containerHasPrintable&... columns);
But this does not work. GCC 13.1
main.cpp|80|error: wrong number of template arguments (1, should be at least 2)| "80 - function"
main.cpp|40|note: provided for 'template<template<class, class> class Container, class Value, class Allocator> concept containerHasPrintable'| "40 - concept"
main.cpp|80|error: expansion pattern 'int&' contains no parameter packs|
How can I constrain the template?
Upvotes: 3
Views: 131
Reputation: 275730
I'd break it down into a few sub-concepts.
This has a few advantages. First, the subsuming rules like it when you do this. Second, you get better error messages -- being told which concepts fail by name. Third, it is somewhat better in self documentation.
template<class Container>
concept IndexedContainer = requires(Container const& data) {
data[0];
std::size(data);
};
template<class T>
concept CanStreamOut = requires(T t, std::ostream& os) {
os << t;
};
template<class C>
concept ElementCanStreamOut = requires(C const& c) {
{ c[0] } -> CanStreamOut;
};
template<class C>
concept PrintableRange = IndexedContainer<C> && ElementCanStreamOut<C>;
The const&
portion helps block std::map
from passing these tests.
Upvotes: 1
Reputation: 303337
This:
template <template <typename, typename> class Container,
typename Value,
typename Allocator = std::allocator<Value>>
concept containerHasPrintable = /* ... */;
Is a concept that is constraining three things: a template and two types. But that's not actually what you want - you want a concept that is constraining one thing: a type.
So what you want to write is just that:
template <typename Container>
concept PrintableContainer = requires (std::ostream& out, Container& data)
{
out << data[0];
};
Note that none of the other stuff in your concept is actually relevant - you don't need the container to specifically look like C<V, A>
anyway - and this just arbitrarily rejects some other types that would otherwise work (like, say, std::string
).
Upvotes: 5