Search code examples
c++algorithmtemplatesc++20function-templates-overloading

Template deduction depends on another template deduction


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.


Solution

  • 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;
    }