为啥原始类型和用户定义类型在从函数返回为“const”时表现不同?

Posted

技术标签:

【中文标题】为啥原始类型和用户定义类型在从函数返回为“const”时表现不同?【英文标题】:Why do primitive and user-defined types act differently when returned as 'const' from a function?为什么原始类型和用户定义类型在从函数返回为“const”时表现不同? 【发布时间】:2017-10-03 12:54:39 【问题描述】:
#include <iostream>

using namespace std;

template<typename T>
void f(T&&)  cout << "f(T&&)" << endl; 

template<typename T>
void f(const T&&)  cout << "f(const T&&)" << endl; 

struct A ;
const A g1()  return ; 
const int g2()  return ; 

int main()

    f(g1()); // outputs "f(const T&&)" as expected.
    f(g2()); // outputs "f(T&&)" not as expected.

问题描述嵌入在代码中。我的编译器是clang 5.0

我只是想知道:

在这种情况下,为什么 C++ 会以不同的方式对待内置类型和自定义类型?

【问题讨论】:

clang编译警告warning: 'const' type qualifier on return type has no effect [-Wignored-qualifiers] 如果构建代码并启用所有警告(-Wall -Wextra on gcc),这些问题本可以避免。 @JonasWielicki:出于向后兼容性的原因,这不会启用所有警告,这就是 Clang 引入 -Weverything 的原因:) @JonasWielicki:基本上,GCC 没有等价物。粗略地说 GCC -Wall 打开所有在引入时存在的警告,但没有任何新警告,因为担心这会阻止使用 -Wall -Werror 的第三方软件的人升级 GCC(这是一个非常有效的担忧,我不应该,看到 GCC 用于构建许多 Linux 发行版)。后来他们添加了-Wextra 来覆盖丢失的警告集,并且出于同样的原因它被稳定了,因此不包括之后包含的警告...... @MatthieuM。 GCC确实不时添加到-Wall-Wextra-Wall 的实际政策类似于“几乎可以肯定表明存在错误,即使没有,也可以轻松更改代码以消除警告”,-Wextra 的实际政策类似,但估计的指示几率较低一个错误。您可能已经注意到,clang 的 -Weverything 包含警告,如果代码按原样正确,则没有解决方法 - 这是 GCC 人们想要避免的。 【参考方案1】:

我没有引用标准,但cppreference 证实了我的怀疑:

非类非数组纯右值不能是 cv 限定的。 (注意:函数调用或强制转换表达式可能会导致非类 cv 限定类型的纯右值,但 cv 限定符会立即被去除。)

返回的const int 只是一个普通的int prvalue,并且使非常量重载比const 更匹配。

【讨论】:

@alfC 因为const A 表示A 的所有成员都不能更改(即使您复制A),而对于int 甚至没有成员存在。 @alfC Looks like 是为了阻止人们用临时类对象做奇怪的事情。 这就留下了为什么 C++ 允许类纯右值是 cv 限定的问题。恕我直言,这与允许它用于非类纯右值一样没有意义。 @cmaster 这有点道理,即它不允许您在类prvalue上调用非常量成员函数。我现在没有示例,但我想可以想出一些场景,例如***.com/q/8716330/3093378。当然在 C++11 中不应该这样做,因为它会使移动语义停止工作。 @vsoftco 抱歉,我仍然看不到允许对类对象进行 cv 限定的意义:如果我正确阅读,prvalue 是具有值语义(!)的操作的瞬态结果,其中prvalue归调用操作的代码所有。恕我直言,限制是否允许调用者修改这样的值不是操作本身的业务。毕竟,调用它的析构函数最终销毁值是调用者的事情。根据定义,这是一个非常量操作,必须允许它避免创建不可删除的临时对象。【参考方案2】:

为什么原始类型和用户定义类型在从函数返回为“const”时表现不同?

因为const 部分已从函数返回的原始类型中删除。原因如下:

在 C++11 中来自 § 5 Expressions [expr](第 84 页):

8

每当一个泛左值表达式作为一个运算符的操作数出现时 期望该操作数的右值,左值到右值(4.1), 数组到指针 (4.2) 或函数到指针 (4.3) 的标准转换是 应用于将表达式转换为纯右值。 [注意:因为 cv-qualifiers 从非类类型表达式的类型中删除 表达式被转换为纯右值,类型为左值表达式 例如,const int 可以用于类型为纯右值表达式的地方 int 是必需的。 ——尾注]

同样来自§ 5.2.3 Explicit type conversion (functional notation) [expr.type.conv](第 95 页):

2

表达式 T(),其中 T 是简单类型说明符或 typename-specifier 用于非数组完整对象类型或 (可能是 cv 限定的)void 类型,创建指定的纯右值 类型,它是 valueinitialized (8.5; 没有为 void() 情况)。 [注意:如果 T 是 cv 限定的非类类型,则 在确定结果的类型时忽略 cv 限定符 prvalue (3.10)。 ——尾注]

这意味着g2() 返回的const int prvalue 被有效地视为int

【讨论】:

【参考方案3】:

引用标准,

§8/6 Expressions [expr]

如果纯右值最初的类型是“cv T”,其中 T 是 cv-unqualified 非类,非数组类型,表达式的类型 在任何进一步分析之前调整为 T。

和§8/9 Expressions [expr]

(强调我的)

当一个泛左值表达式作为一个运算符的操作数出现时 期望该操作数的纯右值,左值到右值, 数组到指针或函数到指针的标准转换是 应用于将表达式转换为纯右值。 [ 注意:因为 从非类表达式的类型中删除 cv 限定符 表达式转换为纯右值时的类型,左值 例如,const int 类型的表达式可以用于纯右值 int 类型的表达式是必需的。 — 尾注 ]

所以对于g2()int是一个非类类型,而(的返回值)g2()是一个prvalue expression,那么const限定符被去掉,所以返回类型不是@ 987654331@,但int。这就是调用f(T&amp;&amp;) 的原因。

【讨论】:

规范措辞在[expr]/6。【参考方案4】:

前面的答案是完全有效的。我只是想添加一个潜在的动机,为什么有时返回 const 对象可能很有用。 在下面的例子中,class A 给出了来自class C 的内部数据的视图,在某些情况下这些数据是不可修改的(免责声明,为简洁起见,省略了一些重要部分——也有可能更简单的方法来实现此行为):

class A 
    int *data;
    friend class C; // allow C to call private constructor
    A(int* x) : data(x) 
    static int* clone(int*) 
        return 0; /* should actually clone data, with reference counting, etc */
    
public:
    // copy constructor of A clones the data
    A(const A& other) : data(clone(other.data)) 
    // accessor operators:
    const int& operator[](int idx) const  return data[idx]; 
    // allows modifying data
    int& operator[](int idx)  return data[idx]; 
;

class C 
    int* internal_data;
public:
    C() : internal_data(new int[4])  // actually, requires proper implementation of destructor, copy-constructor and operator=
    // Making A const prohibits callers of this method to modify internal data of C:
    const A getData() const  return A(internal_data); 
    // returning a non-const A allows modifying internal data:
    A getData()  return A(internal_data); 
;

int main()

    C c1;
    const C c2;

    c1.getData()[0] = 1; // ok, modifies value in c1
    int x = c2.getData()[0]; // ok, reads value from c2
    // c2.getData()[0] = 2;  // fails, tries to modify data from c2
    A a = c2.getData(); // ok, calls copy constructor of A
    a[0] = 2; // ok, works on a copy of c2's data

【讨论】:

以上是关于为啥原始类型和用户定义类型在从函数返回为“const”时表现不同?的主要内容,如果未能解决你的问题,请参考以下文章

为啥结构对齐取决于字段类型是原始类型还是用户定义?

为啥在 C++ 中调用原始类型的构造函数是合法的?

c++中重载输出操作符,为啥要返回引用

R用户定义函数,返回多个类型对象

如何制作一个以用户定义的表类型为参数并在sql中返回相同的函数?

为啥没有为不同的返回类型定义方法重载?