运算符[]的奇怪行为

时间:2011-07-08 20:59:53

标签: c++

在最近几天尝试使用C ++ operator []方法,我遇到了一个我无法解释的异常现象。下面的代码实现了一个简单的测试字符串类,它允许用户通过其下标“operator”方法访问单个字符。由于我想区分左值和右值下标上下文,“operator”方法返回自定义“CReference”类的实例,而不是标准的C ++字符引用。虽然“CReference”“operator char()”和“operator =(char)”方法似乎分别处理每个rvalue和lvalue上下文,但g ++拒绝编译而不存在额外的“operator =(CReference&)”方法,如记录如下。虽然添加此方法似乎安抚了某种编译时依赖项,但在程序执行期间从未在运行时实际调用它。

作为一个认为他们已经对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("%s\n", (const char*)s1);
 return 0;
}

7 个答案:

答案 0 :(得分:3)

  1. 如果您没有自己声明CReference,则行s1[0] = s1[1];会导致编译器为CReference生成隐式复制赋值运算符。这会导致错误,因为您的类具有无法复制的引用成员。

  2. 如果您添加了一个带有const CReference&类型参数的赋值运算符,它将被赋值调用。

  3. 在您的代码中,您声明了类型为void operator=(CReference &p_assignator)的复制赋值运算符。这不能被调用,因为赋值的右侧是临时对象,不能绑定到非const引用。但是,声明此运算符的行为会导致编译器不尝试定义隐式复制赋值运算符,因此避免了先前的编译错误。由于无法调用此运算符,因此编译器会转到另一个带有char类型参数的赋值运算符。

答案 1 :(得分:3)

如果没有operator=(CReference&)的定义,operator=有两种可能的重载:隐式定义的operator=(const CReference&)operator=(char)。当它尝试编译s1[0] = s1[1]时,它会尝试找到operator=的最佳匹配项,这是隐式定义的operator=(const CReference&)。但是,不允许隐式定义,因为CReference包含引用成员,因此您会收到错误。

相反,当您定义operator=(CReference&)时,不再存在隐式定义的赋值运算符。 C ++ 03标准状态的第9.8节第9-10节:

  

9)用户声明的副本分配operator X::operator=是类X的非静态非模板成员函数,只有一个X类型的参数,X&const X&volatile X&const volatile X& 109)

     

10)如果类定义没有显式声明一个复制赋值运算符,则会隐式声明一个。   类X的隐式声明的复制赋值运算符将具有

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

如果
   - B的每个直接基类X都有一个复制赋值运算符,其参数类型为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引用。

答案 2 :(得分:1)

我通过删除重载的=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. 为什么编译器在我发布的代码示例链接&amp;
  2. 中要求CReference& CReference::operator=(const CReference&)
  3. 如果我们为=运算符提供非常量参数,为什么错误会消失,例如CReference& CReference::operator=(CReference&)
  4. 由于在没有显式非常量参数复制构造函数的情况下,编译器会进行某种优化并将s1[1]的参数视为constant,从而要求进行常量参数复制赋值操作

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

答案 3 :(得分:0)

您可以返回char&amp ;.而不是返回CReference。这将消除对CReference类的需要,这对于我在上面的评论中包含的原因是危险的。你可以有两种形式的operator [],一种返回char,另一种返回char&amp;。

答案 4 :(得分:0)

好的,让我们看看:

s1[0] = s1[1];

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

当然,您需要一个接受CReference的赋值运算符。

因此你的声明用粗体字母表示:

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

不正确。

答案 5 :(得分:0)

我认为你的

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...

答案 6 :(得分:0)

更新:一个想法

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


两个函数CReference::operator char()CReference::operator=(char)不代表左值右值

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

编译器执行“s [0] = s [1];”并且基于您的代码,首先,s [x]将转换为CRefernce匿名对象。然后编译器搜索operator=()。由于您提供operator=(char),编译器是“智能”并尝试完成此分配。因此,它将首先调用操作符char()将右侧(CReference(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转换为CReference(在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;
}