Otóż żebyś się nie zdziwił :D Istnieje możliwość zamknięcia tego w jednej funkcji:
template <typename Arg, typename... Args>
void doPrint(std::ostream& out, Arg&& arg, Args&&... args)
{
out << std::forward<Arg>(arg);
using expander = int[];
(void)expander{0, (void(out << ',' << std::forward<Args>(args)),0)...};
}
Działa to na zasadzie ekspandera, który wpakowuje zera do int[] za pomocą listy inicjalizującej z parameter packiem ( int x[] = { args... }; ) i jednocześnie wywołuje wypisywanie (void jest po to zapewne żeby nie tworzyć de facto tej tablicy). Zastosowane są tu nawiasy () i ich zależność, że tylko ostatni argument jest brany pod uwagę ( int x = (0,1); x jest równe 1 ), a reszta się normalnie wykonuje. Przesyłanie std::ostream jest tu opcjonalne. Równie dobrze można samemu "statycznie" dać std::cout.
Jest to już wyższy poziom, ale mogę to jakoś jeszcze uprościć.
I ogólnie to jest piękne w C++, że można robić naprawdę dziwaczne rzeczy.