【问题标题】:decltype() on std::move() of a value capture in a lambda results in an incorrect type在 lambda 中捕获值的 std::move() 上的 decltype() 导致类型不正确
【发布时间】:2020-05-29 21:50:14
【问题描述】:

似乎 Clang (9.0.0) 有问题,或者我对如何指定 decltype() 在标准中工作的理解。参考以下代码,

#include <utility>
#include <string>

template <typename...> class WhichType;

template <typename T>
std::remove_reference_t<T>&& move_v2(T&& t) {
    WhichType<std::remove_reference_t<T>&&>{};
    return static_cast<std::remove_reference_t<T>&&>(t);
}

int main() {
    auto x = std::string{"a"};
    [v = x]() { 
        // move_v2(v);
        // WhichType<decltype(move_v2(v))>{};
        WhichType<decltype(std::move(v))>{};
    }();
}

上面的代码在WhichType 的模板参数中具有编译器输出implicit instantiation of undefined template 'WhichType&lt;std::__1::basic_string&lt;char&gt; &amp;&amp;&gt;',而不是预期的const std::__1::basic_string&lt;char&gt; &amp;&amp;。在move_v2 中使用move_v2WhichType 本身似乎输出正确的东西。

但是,Clang 似乎也像我预期的 https://wandbox.org/permlink/Nv7yXnCbqxjJMVvX 那样对 std::move(v) 表达式进行重载解析。这让我的一些担忧消失了,但我仍然不明白 decltype() 在 lambda 中的行为。

在这种特殊情况https://wandbox.org/permlink/5mhrOzLn5XZO8LNB 中,GCC 似乎没有这种不一致。


如果我对 decltype() 的理解有误,有人可以纠正我,或者指出这个错误在 clang 中出现的确切位置吗?乍一看似乎有点吓人。当在 SFINAE 或类似的东西中使用时,这可能会导致问题。

【问题讨论】:

  • 正确的结果应该是const &amp;&amp;,因为非可变lambda的按值捕获值是const,所以你的理解是正确的。即使在const &amp;&amp;&amp;&amp;:wandbox.org/permlink/ydV79sx7g7l2RcjB 之间,Clang 也能正确地进行重载解析,因此在实践中它不会造成太大问题。
  • @L.F.谢谢,我担心的是存在复杂的部分专业化链并且有人使用 decltype() 调度并选择适当的功能的情况。在这种情况下,他们可能会得到错误的结果。听起来也很难调试:(
  • @JVApen 我认为这也是我的理解——这似乎是一种边缘情况,编译器没有正确处理,或者根据我们对 decltype() 的理解,我们期望不正确。而且我对后者没有足够的信心做出任何判断。我希望对他们对标准的理解更有信心的人可以链接相关部分并将误解解释为答案。
  • 我想这是clang的错误。一起使用 decltype 时,const 似乎作为引用的返回类型丢失了。

标签: c++ clang language-lawyer c++17 decltype


【解决方案1】:

我已经进行了一些挖掘,在我看来,答案比一些 cmets 看起来更微妙。

首先是this answer to an earlier question。引用重要部分:

[C++11: 5.1.2/14]: 如果实体被隐式捕获并且 capture-default=,则它被副本捕获 >如果它是使用不包含&amp; 的捕获显式捕获的。对于通过副本捕获的每个实体,在闭包类型中声明了一个未命名的非静态数据成员。这些成员的声明顺序是未指定的。 如果实体不是对对象的引用,则此类数据成员的类型为相应捕获实体的类型,否则为引用类型。 [..]

然后是this answer to another question。再次引用:

5 非泛型 lambda 表达式的闭包类型有一个 public 内联函数调用运算符 [...] 此函数调用运算符或 当且仅当 lambda-expression 的 parameter-declaration-clause 不跟在后面 mutable.

然后我把它放在一个小测试中:

#include <iostream>
using std::cout;
using std::endl;

void foo(const std::string&) {
    cout << "void foo(const std::string&)" << endl;
}

void foo(std::string&) {
    cout << "void (std::string&)" << endl;
}

struct klaf
{
    std::string b;

    void bla() const
    {
        cout << std::boolalpha << std::is_const<decltype(b)>::value << endl;
        foo(b);
    }
};

int main()
{
    klaf k;
    k.bla();

    std::string s;
    const std::string s2;
    auto lam = [=]() {
        cout << std::boolalpha << std::is_const<decltype(s)>::value << endl;
        foo(s);

        cout << std::boolalpha << std::is_const<decltype(s2)>::value << endl;
        foo(s2);
    };
    lam();
}

哪些输出(GCC、Clang 和 MSVC 中的输出相同):

false
void foo(const std::string&)
false
void foo(const std::string&)
true
void foo(const std::string&)

klaf::b(显然)不是const,但由于klaf::bla 函数是const,那么klaf::b 在对foo 的调用中被视为const

lam 中也是如此,其中s 由类型为std::string 的值捕获。但是,s2 已被声明为 const std::string,并且会延续到 lambda 中数据成员的类型。

简而言之:在 lambda 中按值捕获不会使捕获的成员本身成为 const,但由于 lambda 的 operator()const,因此成员被提升为 @ 987654345@ 在该函数中(除非 lambda 被声明为可变)。

编辑:

受@arnes 评论的启发,我发现 GCC 和 Clang 有所不同:

int main()
{
    int i = 12;
    auto lam = [=, ic = i]() {
        cout << std::boolalpha << std::is_const<decltype(i)>::value << endl;
        cout << std::boolalpha << std::is_const<decltype(ic)>::value << endl;
    };
    lam();
}

这会在 Clang 中生成 false false,但在 GCC 中生成 false true。换句话说,带有初始化程序的捕获在 GCC 中变为 const,但在 Clang 中则不是。

【讨论】:

  • 是的,但奇怪的是 Clang 和 GCC 之间存在冲突,似乎 gcc 是错误的,因为在 auto x = std::string{"a"}; 语句中,x 的类型是 std::string
  • 您的示例做了不同的事情。您将decltype 应用于变量(被特殊处理)而不是表达式。如果您使用std::remove_reference_t&lt;decltype((s))&gt; 而不是decltype(s),那么在所有三种情况下,结果类型都是const
【解决方案2】:

Clang 是错误的。 decltype(std::move(v)) 应该是 const &amp;&amp; 因为 cv 限定符 v(相当于 this-&gt;v)是 *this 的 cv 限定符的并集(这是 const 中的 operator()mutable lambda) 和v (这是无),所以vconst 左值。然后std::move转换为对应类型的xvalue,所以decltype应该是const &amp;&amp;

decltype 在应用于 id 表达式或成员访问表达式 (this-&gt;v) 时表现特别,但这里不是这种情况。 std::move(v) 是一个复杂的表达式,所以它被当作一个普通的表达式来处理。

【讨论】:

    猜你喜欢
    • 2019-03-05
    • 2020-08-17
    • 1970-01-01
    • 2021-08-28
    • 2012-07-09
    • 1970-01-01
    • 2018-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多