Generally, you will need to have static reflection to iterate through the fields and print them, which C++ doesn't have.
Unless you hack the language really hard.
First, you "scan" the struct for field count
struct ubiq
{
template <typename T>
operator T();
};
template <size_t>
using ubiq_t = ubiq;
template <typename T, typename... Ubiqs>
constexpr auto count_r(size_t& sz, int) -> std::void_t<decltype(T{Ubiqs{}...})>
{
sz = sizeof...(Ubiqs);
}
template <typename T, typename, typename... Ubiqs>
constexpr auto count_r(size_t& sz, float)
{
count_r<T, Ubiqs...>(sz, 0);
}
template<typename T, size_t... Is>
constexpr auto count(std::index_sequence<Is...>)
{
size_t sz;
count_r<T, ubiq_t<Is>...>(sz, 0);
return sz;
}
template<typename T>
constexpr auto count()
{
return count<T>(std::make_index_sequence<sizeof(T)>{});
}
Which works by attempting to initialize the struct with ubiq
s, a type which has a fake conversion operator to anything. Combined with SFINAE, you can check how many of them is needed, which will be the field count.
Then, to extract the fields as usable objects without knowing their names, we may use structured bindings. For example, for two fields we can
template <typename T>
inline auto as_tuple(T&& t, std::integral_constant<size_t, 2>)
{
auto& [x0, x1] = t;
return std::forward_as_tuple(x0, x1);
}
This, unfortunately, requires a individual function for each arity. But suppose we have all the arity we need, then we could get and use the fields as
struct S { int x; std::string s; };
S s = {42, "42"};
auto tup = as_tuple(s, std::integral_constant<size_t, count<S>()>{});
std::cout << get<0>(tup) << ' ' << get<1>(tup);
Now, all you have to do is to glue everything together to make it automagically print any aggregate type.
template<typename T>
void println(T&& t)
{
using rT = std::remove_reference_t<T>;
auto tup = as_tuple(t, std::integral_constant<size_t, count<rT>()>{});
std::apply([](auto&... xs) { ((std::cout << xs << ' '), ...); }, tup);
}
It so happens that I already wrote a library ezprint which does it for you (and some more). The end result you get is
ez::println(s); // prints {42 42}
Without defining anything for the custom type.