【问题标题】:Implementation of template of a << operator // C++<< 运算符的模板的实现 // C++
【发布时间】:2021-04-08 13:54:22
【问题描述】:

我想在 C++ 中制作一个

template <typename T>
ostream& operator<<(ostream& os, T something)
{
    os << something.begin() << something.end();
    return os;
}

它并没有真正起作用,我认为有经验的 C++ 程序员可以解释我为什么。

提前感谢您对该问题的任何回答。

【问题讨论】:

  • 我的回答也存在一些缺陷,即检查 std::string 而不是对现有 operator&lt;&lt; 实现进行一般检查。如果@TedLyngmo 有更好的东西,我可以更改/删除它。

标签: c++ templates iterator operators cout


【解决方案1】:

您的重载将匹配几乎所有导致 operator&lt;&lt; 已经重载的类型的歧义。

我怀疑您想在此处打印容器中的所有元素:os &lt;&lt; something.begin() &lt;&lt; something.end();。这将不起作用,因为 begin()end() 返回迭代器。你可以取消引用它们

if(something.begin() != something.end())
    os << *something.begin() << *std::prev(something.end());

但您只会打印第一个和最后一个元素。这将打印所有这些:

for(const auto& v : something) os << v;

要解决歧义问题,您可以使用模板模板参数并为您想要支持的容器启用operator&lt;&lt; 重载。

例子:

#include <deque>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <type_traits>
#include <vector>

// helper trait - add containers you'd like to support to the list
template <typename T> struct is_container : std::false_type {};
template <typename... Ts> struct is_container<std::vector<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::list<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::deque<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::map<Ts...>> : std::true_type{};

// C is the container template, like std::vector
// Ts... are the template parameters used to create the container.
template <template <typename...> class C, typename... Ts>
// only enable this for the containers you want to support
typename std::enable_if<is_container<C<Ts...>>::value, std::ostream&>::type
operator<<(std::ostream& os, const C<Ts...>& something) {
    auto it = something.begin();
    auto end = something.end();
    if(it != end) {
        os << *it;
        for(++it; it != end; ++it) {
            os << ',' << *it;
        }
    }
    return os;
}

另一种方法是使其通用,但禁用已经支持流式传输的类型的重载。

#include <iostream>
#include <iterator>
#include <type_traits>

// A helper trait to check if the type already supports streaming to avoid adding
// an overload for std::string, std::filesystem::path etc.
template<typename T>
class is_streamable {
    template<typename TT>
    static auto test(int) ->
    decltype( std::declval<std::ostream&>() << std::declval<TT>(), std::true_type() );

    template<typename>
    static auto test(...) -> std::false_type;

public:
    static constexpr bool value = decltype(test<T>(0))::value;
};

template <typename T, 
    typename U = decltype(*std::begin(std::declval<T>())), // must have begin
    typename V = decltype(*std::end(std::declval<T>()))    // must have end
>
// Only enable the overload for types not already streamable
typename std::enable_if<not is_streamable<T>::value, std::ostream&>::type
operator<<(std::ostream& os, const T& something) {
    auto it = std::begin(something);
    auto end = std::end(something);
    if(it != end) {
        os << *it;
        for(++it; it != end; ++it) {
            os << ',' << *it;
        }
    }
    return os;
}

注意:最后一个示例在clang++MSVC 中有效,但在g++ 中编译失败(超出递归深度)。

对于带有value_type 本身不可流式传输的容器,例如std::map 中的std::pair&lt;const Key, T&gt;,您需要添加单独的重载。这需要在上述任何模板之前声明:

template <typename Key, typename T>
std::ostream &operator<<(std::ostream &os, const std::pair<const Key, T>& p) {
    return os << p.first << ',' << p.second;
}

【讨论】:

  • 案例中的“...”是什么?我无法真正理解语法
  • 这是一个可变参数模板,... 用于parameter pack
  • 好的,可变参数模板意味着,无论何时我们使用它,它都可以接受不同数量的参数,是吗?因此,一旦我们可以通过 2、3 或任何其他数量。我明白,但语法看起来有点奇怪,我仍然不能真正习惯 cpp 语法。 template class C - 它到底是什么意思,它有什么功能?它是某种容器,如vector、deque、set、map等吗?还有 Ts... 是我们填充容器的值吗?
  • 我也有错误,我不知道这是不是因为我使用的 C++ 版本 - docs.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/…
  • @Insekure 表示C 是一个模板(如std::vector),而不是该模板的实例化(如std::vector&lt;int&gt;)。 Ts... 是用于实例化它的参数。关于错误:您必须使用较旧的 C++ 版本吗?如果您有 VS2019,您可以将语言标准更改为 C++17(或 latest 以获得一些 C++20 支持)。无论如何,我也将答案更改为支持 C++11 和 14。
【解决方案2】:

您的代码有正确的想法,但缺少一些东西。

template <typename T>
ostream& operator<<(ostream& os, T something)
{
    os << something.begin() << something.end();
    return os;
}

可迭代容器(如std::map 等)应通过迭代其所有元素并逐个输出来输出。在这里,您只输出开始和结束 迭代器,它们与元素本身不同。

我们可以改为使用*it 从容器中的迭代器中获取元素。因此,下面的代码将在T 类型的标准容器中输出所有元素。我还包括一些额外的漂亮印刷。

template <typename T>
std::ostream &operator<<(std::ostream &os, const T &o) {
    auto it = o.begin();
    os << "{" << *it;
    for (it++; it != o.end(); it++) {
        os << ", " << *it;
    }
    return os << "}";
}

如果我们只是使用

template <typename T>

在此函数声明之前,它将与现有的&lt;&lt; 运算符声明冲突。也就是我们写std::cout &lt;&lt; std::string("hello world");的时候,是调用我们的函数实现,还是调用&lt;string&gt;的函数实现?当然,如果可用,我们希望使用标准的operator&lt;&lt; 实现。我们通过限制模板来做到这一点,使其仅适用于具有begin()end() 成员的标准容器,但不适用于具有begin()end() 但也具有现有operator&lt;&lt; 实现的std::string我们想要使用的。

template <typename T,
    typename std::enable_if<is_iterable<T>::value, bool>::type = 0,
    typename std::enable_if<!std::is_same<T, std::string>::value, bool>::type = 0>

第二个std::enable_if 很简单:模板应该涵盖类型,只要它们不是std::string。第一个std::enable_if 检查T 类型是否是可迭代的。我们需要自己进行检查。

template <typename T>
class is_iterable {
    private:
    typedef char True[1];
    typedef char False[2];

    template <typename Q,
        typename std::enable_if<
            std::is_same<decltype(std::declval<const Q &>().begin()),
                decltype(std::declval<const Q &>().begin())>::value,
            char>::type = 0>
    static True &test(char);

    template <typename...>
    static False &test(...);

    public:
    static bool const value = sizeof(test<T>(0)) == sizeof(True);
};

is_iterable 有两个版本的函数test。如果begin()end() 存在于类型T 上,则启用第一个版本,并且它们的返回类型相同(有更精确的方法进行检查,但现在就足够了)。第二个版本以其他方式调用。两个版本的返回类型不同,通过查看返回类型的大小,我们可以设置value,当且仅当Titerable时,true(在在我们的例子中,如果T 定义了begin()end() 并且它们的返回类型相同)。

最后,我们注意到std::map&lt;T1, T2&gt; 的元素实际上是std::pair&lt;T1, T2&gt; 类型的,所以我们需要额外重载operator&lt;&lt; 用于模板对。

template <typename T1, typename T2>
std::ostream &operator<<(std::ostream &os, const std::pair<T1, T2> &o) {
    return os << "(" << o.first << ", " << o.second << ")";
}

综合起来,我们可以试试这个。请注意,它甚至适用于嵌套的 iterator 类型,例如 listUnorderedSetTest

#include <iostream>
#include <list>
#include <map>
#include <set>
#include <type_traits>
#include <unordered_set>
#include <vector>

template <typename T>
class is_iterable {
    private:
    typedef char True[1];
    typedef char False[2];

    template <typename Q,
        typename std::enable_if<
            std::is_same<decltype(std::declval<const Q &>().begin()),
                decltype(std::declval<const Q &>().begin())>::value,
            char>::type = 0>
    static True &test(char);

    template <typename...>
    static False &test(...);

    public:
    static bool const value = sizeof(test<T>(0)) == sizeof(True);
};

template <typename T1, typename T2>
std::ostream &operator<<(std::ostream &os, const std::pair<T1, T2> &o) {
    return os << "(" << o.first << ", " << o.second << ")";
}

template <typename T,
    typename std::enable_if<is_iterable<T>::value, bool>::type = 0,
    typename std::enable_if<!std::is_same<T, std::string>::value, bool>::type = 0>
std::ostream &operator<<(std::ostream &os, const T &o) {
    auto it = o.begin();
    os << "{" << *it;
    for (it++; it != o.end(); it++) {
        os << ", " << *it;
    }
    return os << "}";
}

int main() {
    std::vector<std::string> vectorTest{"hello", "world", "!"};
    std::cout << vectorTest << std::endl;

    std::set<const char *> setTest{"does", "this", "set", "work", "?"};
    std::cout << setTest << std::endl;

    std::map<std::string, std::size_t> mapTest{
        {"bob", 100}, {"alice", 16384}, {"xavier", 216}};
    std::cout << mapTest << std::endl;

    std::list<std::unordered_set<std::string>> listUnorderedSetTest{
        {"alice", "abraham", "aria"},
        {"carl", "crystal", "ciri"},
        {"november", "nathaniel"}};
    std::cout << listUnorderedSetTest << std::endl;
    return 0;
}

这个输出:

{hello, world, !}
{does, this, set, work, ?}
{(alice, 16384), (bob, 100), (xavier, 216)}
{{alice, abraham, aria}, {carl, crystal, ciri}, {november, nathaniel}}

Templated check for the existence of a class member function? 上还有很多其他相关的讨论,您可能会觉得有帮助。这个答案的缺点是检查std::string,而不是检查现有的operator&lt;&lt; 实现,我认为可以通过更多的工作来解决decltype 的类型检查。

【讨论】:

  • 我喜欢这个想法,但正如我的回答导致 operator&lt;&lt; 重载已经存在的标准类型的模棱两可的重载一样,这也是。 std::cout &lt;&lt; std::filesystem::path{"."}; 会出现模棱两可的重载等。我认为最好将其限制为一组固定的容器 - 或者,如果可能,使用 sfinae 排除 operator&lt;&lt; 已经有超载。
  • 一个小细节:如果与空容器一起使用,您的 operator&lt;&lt; 示例实现将取消引用 end()
  • @TedLyngmo 不错的收获。
  • @GILGAMESH 谢谢。我想我从来没有像我为此所做的那样删除、取消删除和编辑答案。这样一个看似简单的事情 - 我仍然不能 100% 确定我的通用版本是否正常。我想我会在几天后重新考虑这个问题。:-)
猜你喜欢
  • 2014-12-07
  • 2011-05-23
  • 1970-01-01
  • 2017-05-03
  • 2019-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多