Types - Resurrected¶
Title: Basic Data Types
Link: https://www.hackerrank.com/challenges/c-tutorial-basic-data-types/problem
Iterating Iterators¶
That oxymoron is simply there to remark that for the “Basic Data Types” challenge we actually iterated over the types and we did not see a single iterator along the ways. In spite of all the wonders we applied, if constexpr
, sizeof...
, fold expressions, and std::variant
, we were working in compile-time mode. There was no room for iterators.
But there is always room for improvement, and we are going to find room for the iterators.
Templated Iterators¶
Obviously, if we have been working with templates to address the problem of moving around a list of types, templated iterators seem like the ways to go.
We are going to use two new C++ elements, and as it has been the case, we are going to couple another C++17 present with C++ veterans. The modern element is std::any
, a class capable of holding any value, that has to be retrieved with the proper type, using std::any_cast<T>(any_instance)
. Our veterans will be std::vector
, to store factories of iterators for different types. Those factories will be hosted by a C++11 family member, namely std::function
.
Recall that we previously had different ways of iterating over the list of types. To build something more complex we still need a first iteration over them. We will be building upon the latest solution, using std::variant
and looping with a size_t i
template parameter.
Let us see how the factories and the storage for them are modeled.
using InTypeProcs = std::vector<std::function<std::any(std::istream &)>>;
using OutTypeProcs = std::vector<
std::function<void(std::ostream &, const std::any &, size_t, size_t)>>;
For the input, there will be a vector holding functions that return std::any
and take an std::istream &
as the only parameter. The std::vector
for the output will hold functions that take an std::ostream &
, the value in the form of std::any
, and finally two size_t
parameters. We need those two to pass the values controlling the decimal places for float
and double
.
Let us see how we will use that for the input iterator, which we name vistream_iterator
(where v
stands for variant
).
template <typename Variant, size_t i = 0>
auto
for_each_in(InTypeProcs &intypeprocs) {
auto intypeproc = [](std::istream &is) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto in = std::istream_iterator<BasicType>{is};
return std::any{*in};
};
intypeprocs.push_back(intypeproc);
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each_in<Variant, i + 1>(intypeprocs);
}
vistream_iterator(std::istream &is)
: m_is{is}, m_end{not std::variant_size_v<Variant>} {
for_each_in<Variant>(m_intypeprocs);
}
Just a we did previously, we recurse over the types stored in the std::variant
we receive as a template parameter. For each type a lambda is created and stored in a vector. That lambda will later take over and create an std::istream_iterator
of the corresponding type, returning it embedded in an std::any
instance.
The key to using those lambda expressions is in the *
and ++
operators.
auto operator *() const {
auto intypeproc = *std::next(m_intypeprocs.begin(), m_pos);
return intypeproc(m_is);
}
// Prefix increment
auto operator ++() {
m_end = (++m_pos == std::variant_size_v<Variant>);
return *this;
}
When someone invokes the *
operator, the next lambda is retrieved. Literally, std::next
is always called with an offset to the beginning of the container. That offset is incremented during ++
operations, checking if the end of the container has been reached.
There is of course a corresponding recursion procedure for the output.
template <typename Variant, size_t i = 0>
auto
for_each_out(OutTypeProcs &otypeprocs) {
auto proc = [](std::ostream &os, const std::any &val,
size_t fprec = 3, size_t dprec = 9) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
const auto def_prec{os.precision()}; // save current prec
const auto def_flags{os.flags()}; // save current prec
auto out = std::ostream_iterator<BasicType>{os};
if constexpr (std::is_same_v<float, BasicType>)
os << std::fixed << std::setprecision(fprec);
else if constexpr (std::is_same_v<double, BasicType>)
os << std::fixed << std::setprecision(dprec);
*out++ = std::any_cast<const BasicType &>(val);
(os << std::setprecision(def_prec)).flags(def_flags); // reset
};
otypeprocs.push_back(proc);
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each_out<Variant, i + 1>(otypeprocs);
}
The code in the lambda does the same as we did previously. The only novelty, as is the case with the input, is the pre-creation of the lambdas that go into the std::vector
. The vostream_iterator
in which we have our storage, will later retrieve the corresponding lambda.
auto &operator =(const std::any &val) const {
auto otypeproc = *std::next(m_otypeprocs.begin(), m_pos);
otypeproc(m_os, val, m_fprec, m_dprec);
m_os << m_delim;
return *this;
}
As was the case with our previous custom output iterator the real work takes place in the =
operators. Here also, std::next
is used to fetch the lambda that knows what to do with the std::any
, given also an output stream and the values for the decimal places for float
and double
.
It is now possible to create an STL-like solution. Here is the code for the entire solution.
#include <any> // std::any
#include <algorithm> // std::copy_n
#include <functional> // std::function
#include <iomanip> // std::setprecision
#include <ios> // std::fixed
#include <iostream> // std::cin/cout
#include <iterator> // std::ostream_iterator/istream
#include <type_traits> // std::is_same_v
#include <variant> // std::variant
#include <vector> // std::vector
using InTypeProcs = std::vector<std::function<std::any(std::istream &)>>;
template <typename Variant, size_t i = 0>
auto
for_each_in(InTypeProcs &intypeprocs) {
auto intypeproc = [](std::istream &is) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto in = std::istream_iterator<BasicType>{is};
return std::any{*in};
};
intypeprocs.push_back(intypeproc);
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each_in<Variant, i + 1>(intypeprocs);
}
template <typename Variant>
struct vistream_iterator {
// Iterator tags
using iterator_category = std::input_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
InTypeProcs m_intypeprocs;
size_t m_pos = 0;
std::istream &m_is;
bool m_end = false;
vistream_iterator(std::istream &is)
: m_is{is}, m_end{not std::variant_size_v<Variant>} {
for_each_in<Variant>(m_intypeprocs);
}
// dummy m_is initialization to avoid using -fpermissive
vistream_iterator() : m_is{std::cin}, m_end{true} {}
auto operator ->() const noexcept { return this; }
auto operator *() const {
auto intypeproc = *std::next(m_intypeprocs.begin(), m_pos);
return intypeproc(m_is);
}
// Prefix increment
auto operator ++() {
m_end = (++m_pos == std::variant_size_v<Variant>);
return *this;
}
// Postfix increment
auto operator ++(int) const {
auto tmp = *this;
++(*this);
return tmp;
}
auto operator ==(const vistream_iterator& other) const {
return m_end ? other.m_end : (other.m_end ? false : m_pos == other.m_pos);
};
auto operator !=(const vistream_iterator& other) const {
return not (*this == other);
}
};
///////////////////////////////////////////////////////////////////////////
using OutTypeProcs = std::vector<
std::function<void(std::ostream &, const std::any &, size_t, size_t)>>;
template <typename Variant, size_t i = 0>
auto
for_each_out(OutTypeProcs &otypeprocs) {
auto proc = [](std::ostream &os, const std::any &val,
size_t fprec = 3, size_t dprec = 9) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
const auto def_prec{os.precision()}; // save current prec
const auto def_flags{os.flags()}; // save current prec
auto out = std::ostream_iterator<BasicType>{os};
if constexpr (std::is_same_v<float, BasicType>)
os << std::fixed << std::setprecision(fprec);
else if constexpr (std::is_same_v<double, BasicType>)
os << std::fixed << std::setprecision(dprec);
*out++ = std::any_cast<const BasicType &>(val);
(os << std::setprecision(def_prec)).flags(def_flags); // reset
};
otypeprocs.push_back(proc);
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each_out<Variant, i + 1>(otypeprocs);
}
template <typename Variant>
class vostream_iterator {
public:
// needed for an iterator
using iterator_category = std::output_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
private:
OutTypeProcs m_otypeprocs;
size_t m_pos = 0;
std::ostream &m_os;
std::string m_delim;
size_t m_fprec;
size_t m_dprec;
public:
// constructor replicating the wrapped iterator's constructor
vostream_iterator(std::ostream &os, const std::string &delim,
size_t fprec = 3, size_t dprec = 9)
: m_os{os}, m_delim(delim), m_fprec(fprec), m_dprec(dprec) {
for_each_out<Variant>(m_otypeprocs);
}
// no-ops because only the assignment (= operator does something)
auto operator ->() const { return this; };
auto &operator *() const { return *this; }
auto &operator ++() { ++m_pos; return *this; } // ++prefix
auto operator ++(int) const { //postfix++
auto tmp = *this;
++(*this);
return tmp;
}
// Operation with the wrapped iterator, choosing when to output the sep
auto &operator =(const std::any &val) const {
auto otypeproc = *std::next(m_otypeprocs.begin(), m_pos);
otypeproc(m_os, val, m_fprec, m_dprec);
m_os << m_delim;
return *this;
}
};
// Solution
template<typename I, typename O>
auto
basic_types(I first, I last, O out) {
return std::copy(first, last, out);
}
int
main(int, char *[]) {
using BasicTypes = std::variant<int, long, char, float, double>;
basic_types(
vistream_iterator<BasicTypes>{std::cin},
vistream_iterator<BasicTypes>{},
vostream_iterator<BasicTypes>{std::cout, "\n"}
);
return 0;
}
Criticism¶
Even if we have made our code dynamic and STL-like, we have probably made things overly complex. Especially because there is an interdependency between the input and the output as they have to remain synchronized to ensure the output iterator retrieves the proper lambda to match the type stored in the std::any
instance.
There must be a way to remove that link between the input and output iterators.
Dynamic Type Output¶
Tackling the nature of the input iterator is not going to be easy because it has to respect the ordering of the target types. But for starters, we can do sensible things such as taking the generation of the input functions out of the constructor.
In the case of the output iterator we can go several steps beyond also doing that. std::any
has a very interesting member function named type
. As the name indicates it lets us know things about the type being held because it “returns the typeid of the contained value”. That means we get the following done for us: typeid(type_in_std::any)
, returning the type information in an instance of std::type_info
.
C++11 brought us std::type_index
, a wrapper around the latter to make it possible to use the type information as the key in mapping containers. And that is what is going to help us with a simplification of the output iterator. We first put the output functions in an std::map
using an std::type_index
as the key. When the time comes to do actual output, std::any
, via type()
, gives us a type_info
, that we can wrap inside an std::type_index
to look for the proper function in the std::map
.
using BasicTypes = std::variant<int, long, char, float, double>;
using InTypeProc = std::function<std::any(std::istream &)>;
using InTypeProcs = std::vector<InTypeProc>;
using OutTypeProc = std::function<void(std::ostream &, const std::any &)>;
using OutTypeProcs = std::map<std::type_index, OutTypeProc>;
static InTypeProcs intypeprocs; // storage for the input fetching procs
static OutTypeProcs outtypeprocs; // storage for the output procs
template <typename Variant, size_t i = 0>
auto
for_each(InTypeProcs &itp, OutTypeProcs &otp) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto intypeproc = [](std::istream &is) {
return std::any{*std::istream_iterator<BasicType>{is}};
};
itp.push_back(intypeproc);
auto outtypeproc = [](std::ostream &os, const std::any &a) {
os << std::any_cast<BasicType>(a);
};
auto typeidx = std::type_index(typeid(BasicType));
otp.insert(std::make_pair(typeidx, outtypeproc));
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each<Variant, i + 1>(itp, otp);
}
We have gone the using
way to have a clear view of the containers holding our input/output functions, the ones working directly with std::any
. Iterating over the types of the std::variant
used to hold the types, both the std::vector
and the std::map
are generated.
The input iterator does not change much because it still needs to know the number of types it has to fetch and it still keeps an index with m_pos
.
On the other hand vostream_iterator
is no longer a template based class.
///////////////////////////////////////////////////////////////////////////
class vostream_iterator {
public:
That also means there is no index to track in parallel to the index of the input iterator. We can now have a generic approach to finding the right function by retrieving it from the map with the information from the std::any
instance.
auto &operator =(const std::any &a) const {
const auto def_prec{m_os.precision()}; // save current prec
const auto def_flags{m_os.flags()}; // save current flags
if (a.type() == typeid(float))
m_os << std::fixed << std::setprecision(m_fprec);
else if (a.type() == typeid(double))
m_os << std::fixed << std::setprecision(m_dprec);
// use structured bindings to get our out-type-proc
auto &[_, otproc] = *outtypeprocs.find(std::type_index(a.type()));
otproc(m_os, a);
(m_os << std::setprecision(def_prec)).flags(def_flags); // reset
m_os << m_delim;
return *this;
}
For bonus points, we also use typeid(type)
to find out if the fixed point precision has to be set for float
and double
. We were previously doing that directly in the output procedures with if constexpr
, but can do it dynamically here now thanks to the type()
member function of std::any
.
To make our life easier we use another of the goodies of C++17, i.e.: Structured Bindings. It feels almost like unpacking an iterable in Python.
// use structured bindings to get our out-type-proc
auto &[_, otproc] = *outtypeprocs.find(std::type_index(a.type()));
otproc(m_os, a);
It also feels like magic, because we can unpack the std::pair
held by the map directly into variables, that can be declared as auto
. Just like in Python we use _
as a placeholder for the variable we want to ignore. This will apparently be official in C++26, when one will be able to use officially use the placeholder and do so as many times as needed.
Let us see the complete code of this second solution.
#include <any> // std::any
#include <algorithm> // std::copy_n
#include <functional> // std::function
#include <iomanip> // std::setprecision
#include <ios> // std::fixed
#include <iostream> // std::cin/cout
#include <iterator> // std::ostream_iterator/istream
#include <map> // std::map
#include <type_traits> // std::is_same_v
#include <variant> // std::variant
#include <vector> // std::vector
#include <typeindex> // std::type_index
using BasicTypes = std::variant<int, long, char, float, double>;
using InTypeProc = std::function<std::any(std::istream &)>;
using InTypeProcs = std::vector<InTypeProc>;
using OutTypeProc = std::function<void(std::ostream &, const std::any &)>;
using OutTypeProcs = std::map<std::type_index, OutTypeProc>;
static InTypeProcs intypeprocs; // storage for the input fetching procs
static OutTypeProcs outtypeprocs; // storage for the output procs
template <typename Variant, size_t i = 0>
auto
for_each(InTypeProcs &itp, OutTypeProcs &otp) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto intypeproc = [](std::istream &is) {
return std::any{*std::istream_iterator<BasicType>{is}};
};
itp.push_back(intypeproc);
auto outtypeproc = [](std::ostream &os, const std::any &a) {
os << std::any_cast<BasicType>(a);
};
auto typeidx = std::type_index(typeid(BasicType));
otp.insert(std::make_pair(typeidx, outtypeproc));
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each<Variant, i + 1>(itp, otp);
}
template <typename Variant>
struct vistream_iterator {
// Iterator tags
using iterator_category = std::input_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
size_t m_pos = 0;
std::istream &m_is = std::cin;
bool m_end = true;
vistream_iterator(std::istream &is) :
m_is{is}, m_end{not std::variant_size_v<Variant>} {}
vistream_iterator() {}
auto operator ->() const noexcept { return this; }
auto operator *() const {
auto intypeproc = *std::next(intypeprocs.begin(), m_pos);
return intypeproc(m_is);
}
// Prefix increment
auto operator ++() {
m_end = (++m_pos == std::variant_size_v<Variant>);
return *this;
}
// Postfix increment
auto operator ++(int) const {
auto tmp = *this;
++(*this);
return tmp;
}
auto operator ==(const vistream_iterator& other) const {
return m_end ? other.m_end : (other.m_end ? false : m_pos == other.m_pos);
};
auto operator !=(const vistream_iterator& other) const {
return not (*this == other);
}
};
///////////////////////////////////////////////////////////////////////////
class vostream_iterator {
public:
// needed for an iterator
using iterator_category = std::output_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
private:
std::ostream &m_os;
std::string m_delim;
size_t m_fprec;
size_t m_dprec;
public:
// constructor replicating the wrapped iterator's constructor
vostream_iterator(std::ostream &os, const std::string &delim,
size_t fprec = 3, size_t dprec = 9)
: m_os{os}, m_delim{delim}, m_fprec{fprec}, m_dprec{dprec} {}
// no-ops because only the assignment (= operator does something)
auto operator ->() const { return this; };
auto &operator *() const { return *this; }
auto &operator ++() { return *this; } // ++prefix
auto operator ++(int) const { return *this; }
auto &operator =(const std::any &a) const {
const auto def_prec{m_os.precision()}; // save current prec
const auto def_flags{m_os.flags()}; // save current flags
if (a.type() == typeid(float))
m_os << std::fixed << std::setprecision(m_fprec);
else if (a.type() == typeid(double))
m_os << std::fixed << std::setprecision(m_dprec);
// use structured bindings to get our out-type-proc
auto &[_, otproc] = *outtypeprocs.find(std::type_index(a.type()));
otproc(m_os, a);
(m_os << std::setprecision(def_prec)).flags(def_flags); // reset
m_os << m_delim;
return *this;
}
};
// Solution
template<typename I, typename O>
auto
basic_types(I first, I last, O out) {
return std::copy(first, last, out);
}
int
main(int, char *[]) {
for_each<BasicTypes>(intypeprocs, outtypeprocs);
basic_types(
vistream_iterator<BasicTypes>{std::cin},
vistream_iterator<BasicTypes>{},
vostream_iterator{std::cout, "\n"}
);
return 0;
}
Going The Iterating Mile¶
We started this new chapter because iterating over the list of types was not the goal. That is why we now have solutions based on iterators. Unfortunately, these solutions rely on knowledge of the std::variant
and the containers with helpers to fetch types from std::cin
and put them to std::cout
.
A real success would be to remove those two dependencies and make the code even more generic. And yes, we can!
Let us start by backpedaling a bit. We decided to go for std::any
as the vehicle to pass instances of the types back and forth, because it can hold anything. However, that flexibility comes with a price: getting the value out of an std::any
instance is hard, hence the need to have so many lambda expressions, why we synchronized the iterators and had to work with std::type_info
.
Let us look at three ideas to streamline our solution
-
Given that we are using
std::variant
to convey the types and it can also hold instances of the types, it may be a lot easier to work with it. -
Let our iterator, literally iterate over the container, instead of doing something with the container. I.e.: our custom iterator will work with iterators to traverse the container of lambda expressions.
-
If getting the value out of an
std::variant
, with the right type, is easier than withstd::any
, we may remove the lambda expressions for the output and make the output iterator really generic.
The code generating the lambdas for fetching the values from std::cin
does not change. It is still an std::vector
, holding lambdas, but this time the return value is an std::variant
. Let us show it.
using BasicTypes = std::variant<int, long, char, float, double>;
using InTypeProc = std::function<BasicTypes(std::istream &)>;
using InTypeProcs = std::vector<InTypeProc>;
static InTypeProcs intypeprocs;
template <typename Variant, size_t i = 0>
auto
for_each_in(InTypeProcs &itp) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto intypeproc = [](std::istream &is) {
return Variant{*std::istream_iterator<BasicType>{is}};
};
No lambdas are generated for the output.
The input iterator works with a typename I
, that must be an iterator.
template <typename I>
struct vistream_iterator {
// Iterator tags
using iterator_category = std::input_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
std::istream &m_is = std::cin;
I m_first, m_last;
bool m_end = true;
vistream_iterator(std::istream &is, I first, I last)
: m_is{is}, m_first{first}, m_last{last}, m_end{first == last} {}
vistream_iterator(I last) : m_first{last}, m_last{last} {}
auto operator ->() const noexcept { return this; }
auto operator *() const {
return (*m_first)(m_is); // fetched intypeproc delivers result
}
// Prefix increment
auto &operator ++() {
m_end = ((++m_first) == m_last);
return *this;
}
// Postfix increment
auto operator ++(int) const {
auto tmp = *this;
++(*this);
return tmp;
}
It is used during construction to get the beginning, with first
, and the end, with last
, of the range that holds procedures to fetch the types. Our input iterator no longer needs to know things about the container. Because the range is not directly controlled by our iterator, the second constructor , defining the end of iteration, also needs to know where the end of the real range is. And that it is why it takes also a last
parameter.
Granted, with some SFINAE we could restrict and check that the iterator I
is of type InputIterator and delivers the expected result when dereferenced.
Things are even better for the output iterator. We had already decoupled it from the std::variant
but it still had to rely on a set of external functions (lambdas) to output each type, as the type had to be forcefully extracted from std::any
. With the use of std::variant
things are a lot cleaner.
template<typename V>
auto &operator =(const V &v) const {
const auto def_prec{m_os.precision()}; // save current prec
const auto def_flags{m_os.flags()}; // save current flags
// visit our variant val and get the type in auto format
std::visit([&](auto &arg) { // arg is the value inside variant v
using T = std::decay_t<decltype(arg)>; // deduce actual type
// do something if float or double
if constexpr (std::is_same_v<T, float>)
m_os << std::fixed << std::setprecision(m_fprec);
else if constexpr (std::is_same_v<T, double>)
m_os << std::fixed << std::setprecision(m_dprec);
m_os << arg; // m_os can already work with arg
}, v);
(m_os << std::setprecision(def_prec)).flags(def_flags); // reset
m_os << m_delim;
return *this;
}
Everything happens inside the operator =
method and the reason is that extracting the type from an std::variant
is not that difficult, because we have the help of new and known friends. First
std::visit
that will put into a function of our choosing the argument inside thestd::variant
(or a collection of those)
If we use a generic lambda, i.e.: with an auto
parameter, the type will be worked out for us. But we still need the type and:
-
Coupled with a new friend:
std::decay_t
. This fine piece of machinery removes the reference and gives us the real type behindauto &arg
With the type in the hand, we can again resort to using our compile-time friend and wonder, if constexpr
, to apply specific code for float
and double
as we did in the past.
There is something worth mentioning, the template <typename V>
technique. Because that V
is the parameter to the method. Recall that the input methods will return an std::variant
with the value embedded. Our operator =
does not need to know the types in the template instantiation; this will be worked out by the compiler.
What we could do is add a bit of SFINAE to make sure that V
is an std::variant
, using std::enable_if_t
and a check for the type.
Let us see the complete code of this final solution.
template<typename V>
auto &operator =(const V &v) const {
const auto def_prec{m_os.precision()}; // save current prec
const auto def_flags{m_os.flags()}; // save current flags
// visit our variant val and get the type in auto format
std::visit([&](auto &arg) { // arg is the value inside variant v
using T = std::decay_t<decltype(arg)>; // deduce actual type
// do something if float or double
if constexpr (std::is_same_v<T, float>)
m_os << std::fixed << std::setprecision(m_fprec);
else if constexpr (std::is_same_v<T, double>)
m_os << std::fixed << std::setprecision(m_dprec);
m_os << arg; // m_os can already work with arg
}, v);
(m_os << std::setprecision(def_prec)).flags(def_flags); // reset
m_os << m_delim;
return *this;
}
Done and dust. We have really generic code for both vistream_iterator
and vostream_iterator
, both freed from having to have knowledge of the std::variant
.
Summary¶
The goal in this chapter was to solve the conundrum of using iterators to iterate over types, rather than fully iterate over the types at compile time. And our solutions have also been subject to iteration until we reached that goal, as much as possible.
Bonus Points¶
OK, if you have made it that far, you have seen the cliffhangers about adding SFINAE for the new things. We could not really leave the chapter without adding it.
First, we need to check if the code generating the lambda for type conversion from an std::istream
is really getting an std::variant
. This is the trickiest because one cannot really check for std::variant
because it will always be bound to a template pack.
The trick is therefore to specialize a std::false_type
/ std::true_type
check, where the true part is a specialization containing an std::variant<Args...>
.
// SFINAE to check for a variant
template<typename V>
struct is_variant_t : std::false_type {};
// Need to specialize to consider a template pack, for std::variant
template<typename ...Args>
struct is_variant_t<std::variant<Args...>> : std::true_type {};
template<typename V>
constexpr bool is_variant_v = is_variant_t<V>::value;
template<typename V>
using enable_if_variant = std::enable_if_t<is_variant_v<V>>*;
template <typename Variant, size_t i = 0, enable_if_variant<Variant> = nullptr>
auto
for_each_in(InTypeProcs &itp) {
vistream_iterator
is the next one in need of SFINAE to see if the template parameter I
is really an InputIterator and even more: if it delivers a function returning an std::variant
after being dereferenced and called with an std::istream
. Because that is the expectation when vistream_iterator
is dereferenced itself.
// SFINAE to check for I being an Input iterator and delivering a variant
template <typename T, typename Tag>
constexpr bool is_it_tag_v = std::is_base_of_v<
Tag, typename std::iterator_traits<T>::iterator_category>;
template <typename I>
constexpr bool is_input_v = is_it_tag_v<I, std::input_iterator_tag>;
template <typename I>
constexpr bool input_variant_v = is_variant_v<
typename std::decay_t<decltype(*std::declval<I>())>::result_type>;
template <typename I>
constexpr bool is_input_variant_v = is_input_v<I> && input_variant_v<I>;
template<typename I>
using enable_if_input_variant = std::enable_if_t<is_input_variant_v<I>>*;
template <typename I, enable_if_input_variant<I> = nullptr>
struct vistream_iterator {
Recall that lately we had made a new friend, std::decay_t<T>
, and it comes to the rescue again. But not alone, because it needs a push from the left, i.e.: being prefixed by typename
to be able to finally get down to ::result_type
.
This is because ::result_type
is a dependent name itself, only available after the previous conundrum has been solved. std::function
stores there the return type of the stored function, and that is what we are looking for.
After that comes our template <V> vostream::operator =(const V &v) const
. That V
needs to be a variant. We therefore reuse what we already developed above and apply enable_if_variant<V>
.
For the general case, we have reapplied the rest of the standard SFINAE machinery, to check for input/output iterators that also have compatible types and was being used in the first solution.
It is now that we have a really complete solution. Game over and here is the code.
#include <any> // std::any
#include <algorithm> // std::copy_n
#include <functional> // std::function
#include <iomanip> // std::setprecision
#include <ios> // std::fixed
#include <iostream> // std::cin/cout
#include <iterator> // std::ostream_iterator/istream
#include <type_traits> // std::is_same_v
#include <variant> // std::variant
#include <vector> // std::vector
using BasicTypes = std::variant<int, long, char, float, double>;
using InTypeProc = std::function<BasicTypes(std::istream &)>;
using InTypeProcs = std::vector<InTypeProc>;
static InTypeProcs intypeprocs;
// SFINAE to check for a variant
template<typename V>
struct is_variant_t : std::false_type {};
// Need to specialize to consider a template pack, for std::variant
template<typename ...Args>
struct is_variant_t<std::variant<Args...>> : std::true_type {};
template<typename V>
constexpr bool is_variant_v = is_variant_t<V>::value;
template<typename V>
using enable_if_variant = std::enable_if_t<is_variant_v<V>>*;
template <typename Variant, size_t i = 0, enable_if_variant<Variant> = nullptr>
auto
for_each_in(InTypeProcs &itp) {
using BasicType = std::variant_alternative_t<i, Variant>; // get type
auto intypeproc = [](std::istream &is) {
return Variant{*std::istream_iterator<BasicType>{is}};
};
itp.push_back(intypeproc);
if constexpr ((i + 1) < std::variant_size_v<Variant>)
for_each_in<Variant, i + 1>(itp);
}
// SFINAE to check for I being an Input iterator and delivering a variant
template <typename T, typename Tag>
constexpr bool is_it_tag_v = std::is_base_of_v<
Tag, typename std::iterator_traits<T>::iterator_category>;
template <typename I>
constexpr bool is_input_v = is_it_tag_v<I, std::input_iterator_tag>;
template <typename I>
constexpr bool input_variant_v = is_variant_v<
typename std::decay_t<decltype(*std::declval<I>())>::result_type>;
template <typename I>
constexpr bool is_input_variant_v = is_input_v<I> && input_variant_v<I>;
template<typename I>
using enable_if_input_variant = std::enable_if_t<is_input_variant_v<I>>*;
template <typename I, enable_if_input_variant<I> = nullptr>
struct vistream_iterator {
// Iterator tags
using iterator_category = std::input_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
std::istream &m_is = std::cin;
I m_first, m_last;
bool m_end = true;
vistream_iterator(std::istream &is, I first, I last)
: m_is{is}, m_first{first}, m_last{last}, m_end{first == last} {}
vistream_iterator(I last) : m_first{last}, m_last{last} {}
auto operator ->() const noexcept { return this; }
auto operator *() const {
return (*m_first)(m_is); // fetched intypeproc delivers result
}
// Prefix increment
auto &operator ++() {
m_end = ((++m_first) == m_last);
return *this;
}
// Postfix increment
auto operator ++(int) const {
auto tmp = *this;
++(*this);
return tmp;
}
auto operator ==(const vistream_iterator& o) const {
return m_end ? o.m_end : (o.m_end ? false : m_first == o.m_first);
};
auto operator !=(const vistream_iterator& other) const {
return not (*this == other);
}
};
///////////////////////////////////////////////////////////////////////////
class vostream_iterator {
public:
// needed for an iterator
using iterator_category = std::output_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::any;
using reference = value_type &;
using pointer = value_type *;
private:
std::ostream &m_os;
std::string m_delim;
size_t m_fprec;
size_t m_dprec;
public:
// constructor replicating the wrapped iterator's constructor
vostream_iterator(std::ostream &os, const std::string &delim,
size_t fprec = 3, size_t dprec = 9)
: m_os{os}, m_delim{delim}, m_fprec{fprec}, m_dprec{dprec} {}
// no-ops because only the assignment (= operator does something)
auto operator ->() const { return this; };
auto &operator *() const { return *this; }
auto &operator ++() { return *this; } // ++prefix
auto &operator ++(int) { return *this; } // postfix++
template<typename V, enable_if_variant<V> = nullptr>
auto &operator =(const V &v) const {
const auto def_prec{m_os.precision()}; // save current prec
const auto def_flags{m_os.flags()}; // save current flags
// visit our variant val and get the type in auto format
std::visit([&](auto &arg) { // arg is the value inside variant v
using T = std::decay_t<decltype(arg)>; // deduce actual type
// do something if float or double
if constexpr (std::is_same_v<T, float>)
m_os << std::fixed << std::setprecision(m_fprec);
else if constexpr (std::is_same_v<T, double>)
m_os << std::fixed << std::setprecision(m_dprec);
m_os << arg; // m_os can already work with arg
}, v);
(m_os << std::setprecision(def_prec)).flags(def_flags); // reset
m_os << m_delim;
return *this;
}
};
// SFINAE (extra, input is taken from above) for the general solution
template <typename O>
constexpr bool is_output_v = is_it_tag_v<O, std::output_iterator_tag>;
template<typename I, typename O>
constexpr bool io_iterators_v = is_input_v<I> && is_output_v<O>;
template <typename I, typename O>
using io_type = decltype(*std::declval<O>() = *std::declval<I>());
template<typename, typename, typename = void>
struct io_i2o : std::false_type {};
template<typename I, typename O>
struct io_i2o<I, O, std::void_t<io_type<I, O>>>
: std::true_type {};
template<typename I, typename O>
constexpr bool io_i2o_v = io_i2o<I, O>::value;
template<typename I, typename O>
using enable_if_io = std::enable_if_t<io_iterators_v<I, O> && io_i2o_v<I, O>>*;
// Solution
template<typename I, typename O, enable_if_io<I, O> = nullptr>
auto
basic_types(I first, I last, O out) {
return std::copy(first, last, out);
}
int
main(int, char *[]) {
for_each_in<BasicTypes>(intypeprocs);
basic_types(
vistream_iterator{std::cin, intypeprocs.begin(), intypeprocs.end()},
vistream_iterator{intypeprocs.end()},
vostream_iterator{std::cout, "\n"}
);
return 0;
}