模板参数推导依赖于另一个模板参数推导。

5

由于并非所有地方都支持std::format,而且我不想再添加像fmt这样的大型依赖项,因此我希望能够快速制作自己的to_string解决方案以处理多种类型。以下是代码。

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

现在,如果我编写以下代码,将会获得一些不错的输出。

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]

现在,由于某些原因,如果我删除 stringify<std::vector<int>> 调用,编译器将无法推断出正确的函数。
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&)'

我认为我理解这里发生了什么,但我不知道为什么或如何修复它。似乎编译器需要手动实例化stringify<std::vector<int>>,以便可以解析stringify<std::vector<std::vector<int>>>

我以前从未遇到过这种行为,并且不知道如何继续。我正在使用Windows上的GCC编译C++20。谢谢。


stringify_inner 函数之前提前声明你的最后一个函数可能会解决这个问题。stringify_inner 只知道整数类型的特化,因此假定未实现的第一个声明是它应该使用的声明。 - Alan Birtles
Clangd 对此会发出警告:inline function 'stringify<std::vector<int>>' is not defined [-Wundefined-inline] - Enlico
@Enlico 这个警告是有效的,但似乎并不影响代码。我其实不确定如何消除这个警告,除了移除 constexpr,但那是另一个时间的问题。 - Jaan
@AlanBirtles 我不确定你的意思。我已经“前向声明”了基础模板函数。如果你指的是stringify<std::vector<int>>,我认为这将打败使用std::ranges::range<T>获取任何有效范围的目的。 - Jaan
不,我的意思是前向声明特化。 - Alan Birtles
@JeJo 编辑:我误读了你的回答。我理解为什么使用单个函数实现可能更容易,但是我认为由于需要处理更多的边角情况,例如包含容器或需要递归展开的元组,因此单个函数可能更加困难。 - Jaan
3个回答

3
你的模板重载声明顺序将导致
template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify(const Type& data) noexcept;

当专门化时,需要考虑负载过载的情况。

template<typename Type> requires std::ranges::range<Type>
constexpr std::string stringify_inner(const Type &data) noexcept {
    return "[" + stringify(data) + "]";
}

使用 Type = std::vector<int>,但该函数未在任何地方定义。您需要确保及早声明范围的函数签名,以便编译器可以使用它:

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

啊,我明白了。谢谢你的回答。我想在这里提前声明每个独特的 stringify ... requires 子句可能是一个好主意。 - Jaan

1
答案是以散文形式在cppreference网站上 cppreference 中输入的。

专业化必须在导致隐式实例化的第一次使用之前声明

在您的示例中,rangestringify专门化将由第一个stringify_inner重载的调用来实例化,但它是在其后面而不是之前声明的。


正如经常发生的那样,通过查看clang对代码的看法,我们可以得到一些见解。

Clangd does give a warning for this: `inline function 'stringify<std::vector<int>>' is not defined [-Wundefined-inline]`.
constexpr std::string stringify(const Type &data) noexcept;
                     ^
somesource.cpp:22:18: note: used here
   return "[" + stringify(data) + "]";
                ^
1 warning generated.

这将比GCC的清晰得多。


相关问答另一个答案的相关评论


1
其他答案提到了函数重载和声明问题。为了未来的读者,我建议将字符串化放在一个单独的(递归)函数中,它可以处理范围和std::integral(或is_stringable)重载可以保留给整数类型。类似以下内容:
#include <type_traits>
#include <string>
#include <concepts>
#include <ranges>
using namespace std::string_literals;

template<typename Type> // concept for checking std::to_string-able types
concept is_stringable = requires (Type t) 
            { {std::to_string(t) }->std::same_as<std::string>; };

// "stringify" overload for is_stringable
constexpr std::string stringify(const is_stringable auto& data) {
    return std::to_string(data);
}

// "stringify" overload for ranges
constexpr std::string stringify(const std::ranges::range auto& data) {
    // value type of the ranges (Only Sequence ranges)
    using ValueType = std::remove_const_t<
        std::remove_reference_t<decltype(*data.cbegin())>
    >;

    if constexpr (is_stringable<ValueType>) {
        std::string string{};
        for (ValueType element : data)
            string += stringify(element) + ", "s;
        string.pop_back();
        string.pop_back();
        return "["s + string + "]"s;
    }
    // else if constexpr (<other types Ex. ValueType == std::tuple>) {}
    // .... more
    else {
        std::string string;
        for (const ValueType& innerRange : data)
            string += stringify(innerRange);
        return string;
    }
}

See live demo in godbolt.org


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接