Since std::format
isn't supported everywhere, and I didn't want another large dependency like fmt
, I wanted to quickly roll my own to_string
solution for a number of types. The following is the code.
#include <ranges>
#include <string>
#include <concepts>
template<typename Type>
constexpr std::string stringify(const Type &data) noexcept;
template<typename Type> requires std::integral<Type>
constexpr std::string stringify(const Type &data) noexcept {
return std::to_string(data);
}
template<typename Type>
constexpr std::string stringify_inner(const Type &data) noexcept {
return stringify(data);
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify_inner(const Type &data) noexcept {
return "[" + stringify(data) + "]";
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type &data) noexcept {
std::string string;
for (auto &i : data) {
string += stringify_inner(i);
string += ", ";
}
string.pop_back();
string.pop_back();
return string;
}
Now, if I write the following code, I get some nice output.
int main() {
std::vector<int> a = { 1, 2, 3, 4 };
std::vector<std::vector<int>> b = {{ 1, 2 }, { 3, 4 }};
std::cout << stringify(a) << std::endl;
std::cout << stringify(b) << std::endl;
}
// >>> 1, 2, 3, 4
// >>> [1, 2], [3, 4]
Now, for some reason, if I remove the stringify<std::vector<int>>
call, the compiler fails to deduce the correct function.
int main() {
// std::vector<int> a = { 1, 2, 3, 4 };
std::vector<std::vector<int>> b = {{ 1, 2 }, { 3, 4 }};
// std::cout << stringify(a) << std::endl;
std::cout << stringify(b) << std::endl;
}
// >>> undefined reference to `std::__cxx11::basic_string<char, std::char_traits<char>,
// >>> std::allocator<char> > stringify<std::vector<int, std::allocator<int> > >(std::vector<int,
// >>> std::allocator<int> > const&)'
I think I understand what is happening here, but I don't know why exactly or how to fix it. It seems like the compiler needs the manual instantiation of stringify<std::vector<int>>
, so that it can resolve stringify<std::vector<std::vector<int>>>
.
I've never encountered this behavior before and have no idea how to continue. I'm compiling with C++20, using GCC on Windows. Thanks.
The order of declarations of your template overloads results in
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept;
being for the overload, when specializing
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify_inner(const Type &data) noexcept {
return "[" + stringify(data) + "]";
}
with Type = std::vector<int>
, but this function isn't defined anywhere. You need to make sure to declare the function signature for ranges early enough for the compiler to use it:
template<typename Type>
constexpr std::string stringify(const Type& data) noexcept;
template<typename Type> requires std::integral<Type>
constexpr std::string stringify(const Type& data) noexcept {
return std::to_string(data);
}
/////////////////////// Add this ////////////////////////////////////
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept;
/////////////////////////////////////////////////////////////////////
template<typename Type>
constexpr std::string stringify_inner(const Type& data) noexcept {
return stringify(data);
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify_inner(const Type& data) noexcept {
return "[" + stringify(data) + "]";
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept {
std::string string;
for (auto& i : data) {
string += stringify_inner(i);
string += ", ";
}
string.pop_back();
string.pop_back();
return string;
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept;
template<typename Type>
constexpr std::string stringify_inner(const Type& data) noexcept {
return stringify(data);
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify_inner(const Type& data) noexcept {
return "[" + stringify(data) + "]";
}
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept {
std::string string;
for (auto& i : data) {
string += stringify_inner(i);
string += ", ";
}
string.pop_back();
string.pop_back();
return string;
}