【问题标题】:Argument type deduction, references and rvalues参数类型推导、引用和右值
【发布时间】:2011-01-02 13:23:11
【问题描述】:

考虑以下情况:函数模板需要转发参数同时保持它的左值性,以防它是非 const 左值,但它本身与参数实际是什么无关,如下所示:

template <typename T>
void target(T&) {
    cout << "non-const lvalue";
}

template <typename T>
void target(const T&) {
    cout << "const lvalue or rvalue";
}


template <typename T>
void forward(T& x) {
    target(x);
}

x 是右值时,而不是将T 推导出为常量类型,它会报错:

int x = 0;
const int y = 0;

forward(x); // T = int
forward(y); // T = const int
forward(0); // Hopefully, T = const int, but actually an error
forward<const int>(0); // Works, T = const int

似乎forward 处理右值(不调用显式模板参数)需要有一个forward(const T&amp;) 重载,即使它的主体是完全重复的。

有什么办法可以避免这种重复吗?

【问题讨论】:

  • 如果你像这样超载左值,你应该被枪杀。

标签: c++ templates reference type-inference rvalue


【解决方案1】:

这是一个已知问题,也是 C++0x 中右值引用的目的。该问题在 C++03 中没有通用解决方案。

发生这种情况有一些古老的历史原因,这是非常没有意义的。我记得问过一次,答案让我很沮丧。

【讨论】:

  • 我是负责这个健全规则的人之一。类型变量的扣除可能只计算值类型。 “const T”不是值类型。这需要的重复不是毫无意义的,而是语义上必不可少的。如果您不知道,右值永远不会有“const”类型,它们实际上是非常可变的。瑞典的 Dag Brueck 负责那个,所以你可以调用 g (f(x).modify()) 来正确建立一些值。
  • @Ytt:这里的“值类型”到底是什么意思?
  • @Yttrill:对。所以右值是完全可变的——但它们不能绑定到可变引用。因此,如果您有g(f(x).modify()),那很好,但您永远不可能有g(f(x)),其中 g 调用会修改自身。我不太确定你是否提出了反对我关于这种事态荒谬的建议的论据。
  • 值的类型。基本上没有顶级常量或引用。指针是值,所以“int const*”是值类型,但“int const”不是。
  • @Yttrill:我认为您在这里忽略了重点。问题不在于 cv-qualifiers 或 reference-ness 是否应该是 typename 参数值的一部分——事实是它们是。鉴于此,问题是,在这种情况下没有“正确”推断出右值有什么重要意义?
【解决方案2】:

一般来说,模板的这个讨厌的问题应该需要重复,因为变量是否为 const 或不是引用的语义是完全不同的。

对此的 C++11 解决方案是“decltype”,但这是一个坏主意,因为它所做的只是复合且已经损坏的类型系统。

无论标准或委员会怎么说,“const int”都不是也永远不会是类型。 “int&”也不会是一种类型。因此,不应允许模板中的类型参数绑定到此类非类型,谢天谢地,情况就是这样。不幸的是,您仍然可以明确强制这种无原则的替换。

有一些愚蠢的规则试图“解决”这个问题,例如“const const int”减少到“const int”,我什至不确定如果你得到“int & &”会发生什么:记住标准不将“int&”算作一种类型,有一个“int lvalue”类型,但这是不同的:

int x;       // type is lvalue int
int &y = x;  // type is lvalue int

这个问题的正确解决方案其实很简单:一切都是可变对象。扔掉“const”(它没那么有用)并扔掉引用、左值和右值。很明显,所有类类型都是可寻址的,无论是否右值(“this”指针就是地址)。委员会试图禁止分配和寻址右值是徒劳的。寻址案例有效,但很容易用微不足道的演员表逃脱。赋值的情况根本不起作用(因为赋值是一个成员函数并且右值是非常量的,你总是可以赋值给一个类型为右值的类)。

无论如何,模板元编程人群都有“decltype”,通过它您可以找到包含任何“const”和“&”位的声明的编码,然后您可以使用各种库运算符分解该编码。以前无法做到这一点,因为该信息实际上不是类型信息(“ref”实际上是存储分配信息)。

【讨论】:

  • “谢天谢地,情况就是这样”。在示例中,forward(y) 为 T 推导出 const int
  • (1) “const int 不是...一种类型”?它显然所有版本的 C++ 中的一个类型,所以我认为你的意思是它不应该是一个类型,在一些理想的假设世界中——对吧? (2) 模板参数推导当然可以推断出const intint&amp;,不知道你在说什么。 (3) 当您说解决方案是“一切都是可变对象”时,您是在提议改变 C++ 语言吗?我只能这样假设,但我最初认为您提倡只使用 C++ 的一个子集来避免 const...
  • ... 但这不适用于转发,因为 T&amp;T 现在是您唯一可能的选择,而 T&amp; 无法匹配右值,而 T 将被推导出为非引用类型,所以它不适用于需要修改传入值的函数。
  • "const int" 不是“尽管标准说了什么”的类型。所以你不能在这里引用标准。它不是一种类型。什么构成类型取决于类型理论,而不是标准文档。
  • 我不理解抱歉...您似乎在举一个例子,说明现有 C++ 规则做正确的事情(禁止编译),但如果您认为现有的 C++ 规则是错误的(我相信你是)我想看一个他们确实失败的例子。
【解决方案3】:

x 是右值时

x 永远不是右值,因为名称是左值。

有没有办法避免这种重复?

C++0x 中有一个方法:

#include <utility>

template <typename T>
void forward(T&& x)   // note the two ampersands
{
    target(std::forward<T>(x));
}

由于引用折叠规则,表达式std::forward&lt;T&gt;(x) 与您自己的转发函数的参数具有相同的值类别。

【讨论】:

    【解决方案4】:

    假设有 k 个参数,据我了解,C++03 中唯一的“解决方案”是手动写出 2^k 个转发函数,采用 &amp;const&amp; 参数的所有可能组合。为了说明,假设target() 实际上采用了 2 个参数。然后你需要:

    template <typename T>
    void forward2(T& x, T& y) {
        target(x, y);
    }
    
    template <typename T>
    void forward2(T& x, T const& y) {
        target(x, y);
    }
    
    template <typename T>
    void forward2(T const& x, T& y) {
        target(x, y);
    }
    
    template <typename T>
    void forward2(T const& x, T const& y) {
        target(x, y);
    }
    

    显然,对于较大的 k,这变得非常笨拙,因此正如其他答案所提到的,C++0x 中的右值引用。

    【讨论】:

    • 你永远不应该这样做,所以永远不需要这样做。
    • @Yttrill:嗯?能够转发任意函数调用将非常有用。请参阅open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm 顶部的“激励示例”部分。
    • @j_random_hacker:我当然同意转发很有用。没有用的是重载非值类型。你永远不应该有两个同名的函数,其中参数仅在“const”或“ref”中有所不同,因为语义不同,所以名称也应该如此。
    • 只是为了确保理解:在 OP 的示例中,我们恰好有这种极其糟糕的做法:两个函数称为 target,一个接受 T&,另一个接受 T const&。我对编写此类函数的任何人都没有同情,因此对于尝试使用通用模板根据其参数 decltype 来定位其中一个或另一个的人也没有任何同情。
    • @Yttrill:我同意重载非值类型是个坏主意。但有时您需要能够转发到一个未知函数您不知道其参数类型(也就是说,它们可能是按值或按引用),在这种情况下您仍然需要完善的转发机制。例如。想象上面的所有forward2() 函数模板都是在U 上模板化的类中的方法,它有一个成员U u;,并且上面对target(x, y) 的调用被更改为u.target(x, y);。现在我们不知道 U::target() 期望什么参数风格,所以 4 个重载是必要的。
    猜你喜欢
    • 1970-01-01
    • 2011-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-27
    • 1970-01-01
    • 1970-01-01
    • 2011-07-13
    相关资源
    最近更新 更多