操作符[]的奇怪行为

Strange behaviour with operator[]

本文关键字:操作符      更新时间:2023-10-16

在最近几天试验了c++操作符[]方法后,我遇到了一个我无法轻易解释的异常现象。下面的代码实现了一个简单的测试字符串类,它允许用户通过下标"操作符"方法访问单个字符。由于我想区分左值下标上下文和右值下标上下文,"operator"方法返回自定义"reference"类的实例,而不是标准的c++字符引用。虽然"CReference" "operator char()"answers"operator=(char)"方法似乎分别处理每个右值和左值上下文,但如果没有下面说明的额外的"operator=(CReference&;)"方法,c++将拒绝编译。虽然添加此方法似乎是为了消除某种编译时依赖,但在程序执行期间,它实际上从未在运行时被调用。

对于那些认为自己已经对c++的复杂性有了基本了解的人来说,这个项目无疑是一次令人谦卑的经历。如果有人能告诉我这是怎么回事,我将永远感激不尽。与此同时,我将不得不拿出c++书籍,以调和我所知道的和我认为知道的之间的空白。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
// Class for holding a reference to type char, retuned by the "operator[](int)"
// method of class "TestString". Independent methods permit differentiation
// between lvalue and rvalue contexts.
class CReference
{
 private:
   char& m_characterReference;
 public:
   // Construct CReference from char&
   CReference(char& m_initialiser)
     : m_characterReference(m_initialiser) {}
   // Invoked when object is referenced in a rvalue char context.
   operator char()
   {
    return m_characterReference;
   }
   // Invoked when object is referenced in a lvalue char= context.
   char operator=(char c)
   {
    m_characterReference = c;
    return c;
   }
   // NEVER INVOKED, but will not compile without! WHY???
   void operator=(CReference &p_assignator){}
};

// Simple string class which permits the manipulation of single characters
// via its "operator[](int)" method.
class TestString
{
 private:
   char m_content[23];
 public:
   // Construct string with test content.
   TestString()
   {
    strcpy(m_content, "This is a test object.");
   }
   // Return pointer to content.
   operator const char*()
   {
    m_content[22] = 0;
    return m_content;
   }
   // Return reference to indexed character.
   CReference operator[](int index)
   {
    return m_content[index];
   }
};

int main(int argc, char *argv[])
{
 TestString s1;
 // Test both lvalue and rvalue subscript access.
 s1[0] = s1[1]; 
 // Print test string.
 printf("%sn", (const char*)s1);
 return 0;
}
  1. s1[0] = s1[1];行会导致编译器为reference生成一个隐式的复制赋值操作符,如果你自己没有声明一个的话。这会导致一个错误,因为你的类有一个引用成员,不能被复制。

  2. 如果您添加了一个接受const CReference&类型形参的赋值操作符,它将被赋值操作符调用。

  3. 在你的代码中,你声明了一个类型为void operator=(CReference &p_assignator)的拷贝赋值操作符。这是不能调用的,因为赋值的右边是一个临时对象,不能绑定到非const引用。然而,声明该操作符的行为使编译器不会尝试定义隐式复制赋值操作符,从而避免了前面的编译错误。因为这个操作符不能被调用,编译器会选择另一个赋值操作符,它接受char类型的形参。

如果没有operator=(CReference&)的定义,那么operator=就有两种可能的重载:隐式定义的operator=(const CReference&)和您的operator=(char)。当它试图编译s1[0] = s1[1]时,它试图找到operator=的最佳匹配,即隐式定义的operator=(const CReference&)。但是,这是不允许隐式定义的,因为CReference包含一个引用成员,所以你会得到一个错误。

相反,当您定义operator=(CReference&)时,不再有隐式定义的赋值操作符。c++ 03标准的12.8条款9-10:

9)用户声明的复制赋值operator X::operator=是类X的非静态非模板成员函数,只有一个形参类型为XX&const X&volatile X&const volatile X&109)

10)如果类定义没有显式声明复制赋值操作符,则隐式声明。类X隐式声明的复制赋值操作符的形式为

X& X::operator=(const X&)

如果
X的每个直接基类B都有一个拷贝赋值操作符,其形参类型为const B&const volatile B&B,且
-对于X中所有属于M类类型(或其数组)的非静态数据成员,每个此类类类型有一个拷贝赋值操作符,其参数类型为const M&const volatile M&M。<一口> 110)

否则,隐式声明的复制赋值操作符的形式为
X& X::operator=(X&)
因此,当它尝试编译s1[0] = s1[1]时,它有两个重载选择:operator=(CReference&)operator=(char)。它选择operator=(char)作为更好的重载,因为它不能将临时对象(s1[1]的结果)转换为非const引用。

我通过删除重载的=在Ideone上编译了您的程序,它给出了以下错误:

prog.cpp: In member function ‘CReference& CReference::operator=(const CReference&)’:
prog.cpp:9: error: non-static reference member ‘char& CReference::m_characterReference’, can't use default assignment operator
prog.cpp: In function ‘int main(int, char**)’:
prog.cpp:70: note: synthesized method ‘CReference& CReference::operator=(const CReference&)’ first required here 

如错误所示,

s1[0] = s1[1];

需要CReference类的复制赋值操作符。

CReference类中有一个引用成员变量

char& m_characterReference;

对于具有引用成员的类,您需要提供自己的=操作符实现,因为默认的=无法推断对引用成员做什么。因此,您需要提供自己版本的=操作符。

这里有一点推测:

,

s1[0]=s1[1]

理想情况下,要调用CReference& CReference::operator=(const CReference&),参数应该是const CReference&,但参数s1[1]返回一个非常量CReference&,所以:

  1. 为什么编译器要求CReference& CReference::operator=(const CReference&)在代码示例链接我张贴&
  2. 为什么如果我们提供=操作符与非常量参数,如CReference& CReference::operator=(CReference&)
  3. 错误消失

在没有显式的非常量参数复制构造函数的情况下,编译器进行了某种优化,并将s1[1]的参数视为constant,从而要求执行常量参数复制赋值操作符。

如果显式提供了非常量参数复制赋值操作符,编译器不会执行其优化,而只使用已提供的操作符。

不返回reference,而是返回char&这将消除对reference类的需求,由于我在上面的评论中提到的原因,这是危险的。操作符[]可以有两种形式,一种返回char,另一种返回char&。

好的,让我们看看:

s1[0] = s1[1];

s1[0]返回CReference
s1[1]返回CReference

当然你需要一个接受CReference的赋值操作符

因此你的语句用粗体表示:

// NEVER INVOKED, but will not compile without! WHY???
void operator=(CReference &p_assignator){}

是不正确的。

我想你

void operator=(CReference &p_assignator){}

之后被调用
// Test both lvalue and rvalue subscript access.
 s1[0] = s1[1]; 

使用该函数,因为s1[0]和s1[1]都返回CReference。而不是char .

你也想做这样的事情

CReference & operator=(CReference &p_assignator){}

处理

s1[0] = s1[1] = s1[2];  //etc...

更新:一个想法

为什么需要提供用户定义的复制赋值操作符?答:我认为隐式赋值操作符的默认行为是将引用的成员变量重新赋值为引用另一个位置,而不是m_characterReference = p_assignator.m_characterReference;中的语义。引用变量对我们来说是一个语法糖,但对编译器来说它是一个常量指针。因此,默认赋值会尝试重新赋值该常量指针。


CReference::operator char()CReference::operator=(char)两个函数不表示lvaluervalue

如前所述,您的主要错误是将复制赋值操作符声明为返回void。因此,它不会被调用。

为了让编译器执行"s[0] = s[1];",根据你的代码,首先,s[x]将被转换为reference匿名对象。然后编译器搜索operator=()。由于您提供了operator=(char),编译器是"智能的",并尝试完成此任务。因此,它将首先调用operator char()将右侧(其中reference (s[1])转换为char,然后调用operator=(char)函数。表达式(s[0] = s[1])变成char类型。

您可以通过将代码修改为 来避免这种转换。
class CReference
{
private:
  char& m_characterReference;
public:
  // Construct CReference from char&
  CReference(char &m_initialiser)
: m_characterReference(m_initialiser) {}
  char& operator = (const CReference &r) {
  m_characterReference = r.m_characterReference;
  return m_characterReference;
  }
};

如果您希望保持"s1[0] = s1[1]"的类型为字符,则赋值操作符返回char。你不应该担心从char到reference的转换(在s1[0] = s1[1] = s1[2]的情况下),因为1-arg构造函数将处理这种转换。

表示左值和右值的一个简单明了的例子是

class Point {
    public:
        int& x() {return x_value; }
        int& y() {return y_value; }
    private:
        int x_value;
        int y_value;
};
int main()
{
    Point p1;
    p1.x() = 2;
    p1.y() = 4;
    cout << p1.x() << endl;
    cout << p1.y() << endl;
}