为什么我的类使用了错误的 operator()?

Why is my class using the wrong operator()?

提问人:cnewbie 提问时间:10/31/2023 最后编辑:user12002570cnewbie 更新时间:10/31/2023 访问量:117

问:

考虑以下类,它提供了 2 个运算符,一个用于读取,另一个用于写入。Foo()

#include <iostream>
#include <vector>

template <typename T>
class Foo {
 public:
  Foo(const std::vector<T> &values) { vals = values; }
  const T &operator()(const int i, const int j) const {
    std::cout << "Read" << std::endl;
    return vals[i];
  }
  T &operator()(const int i, const int j) {
    std::cout << "Write" << std::endl;
    return vals[i];
  } 

 private:
  std::vector<T> vals;
};

int main() {
  std::vector<int> values(100, 1);

  Foo<int> f{values};
  f(1,1) = 42; //here it writes:OK
  std::cout << f(1,1) << std::endl; //why is again the "read" version called?
  return 0;
}

我无法理解为什么在我的代码中只调用“写入”版本。

C++ 运算符重载

评论

0赞 Ted Lyngmo 10/31/2023
写入版本不需要任何转换,因此它是最佳匹配
0赞 cnewbie 10/31/2023
您能否详细说明一下?我不明白你的意思@TedLyngmo
0赞 Ted Lyngmo 10/31/2023
f是非,因此写入版本不需要转换来制作 A。顺便说一句,这真的重要吗?constthisconst Foo<int>*
0赞 cnewbie 10/31/2023
但是,在这种情况下,我该如何编写“读取”版本呢?我当然希望将读取称为@TedLyngmostd::cout << f(1,1) << std::endl;
4赞 JaMiT 10/31/2023
看看语法和你写的东西;忽略你的意图。一行有(子)表达式,另一行有(子)表达式。为什么编译器应该以不同的方式处理这些相同的表达式?f(1,1)f(1,1)

答:

3赞 user12002570 10/31/2023 #1

问题是它是非常量,这意味着非常量成员函数版本比常量成员函数版本匹配,因为非常量版本的参数类型是,而常版本的参数类型是,因此后者需要限定转换,而前者不需要,因此匹配度更好fthisFoo<int>*const Foo<int>*

要解决这个问题,你可以制作 const(但这样你就不能在上面写了)或者在 non-const 上使用,如下所示:fconst_castf

int main() 
{
     std::vector<int> values(100, 1);

     Foo<int> f{values};  //non-const f
     f(1,1) = 42; //uses write version
     //use read version now as we've used const_cast
     std::cout << const_cast<const Foo<int>&>(f)(1, 1) << '\n'; //use const_cast 
     return 0;
}

工作演示


在 C++17 中,您还可以使用 std::as_const,如此演示中所示。

评论

1赞 user12002570 10/31/2023
@WeijunZhou完成,添加到更新的答案中。std::as_const
0赞 James Kanze 10/31/2023 #2

正如其他人所解释的,运算符重载解析选择写入函数,因为它是在非常量对象上调用的。重载分辨率(通常)不考虑结果的使用方式。它只考虑参数的类型。

这里通常的解决方案是让写入重载返回代理;将运算符的实际代码放在两个不同的函数中(例如,和)。代理将简单地保存指向实际对象的指针和两个参数。它将有一个转发到函数的赋值运算符,以及一个转发到函数的转换。(解析类型转换时,运算符是运算符重载解析考虑目标类型的罕见情况之一。putgetputoperator T() constget

像这样的东西应该可以工作:

#include <iostream>
#include <vector>

template <typename T>
class Foo
{
    class Proxy
    {
    public:
        Proxy( Foo* owner, int i, int j )
            :   m_owner( owner )
            ,   m_i( i )
            ,   m_j( j )
        {
        }
        Proxy& operator=( T const& t )
        {
            m_owner->put( m_i, m_j, t );
            return *this;
        }
        operator T const&() const
        {
            return m_owner->get( m_i, m_j );
        }
    private:
        Foo*            m_owner;
        int             m_i;
        int             m_j;
    };

public:
    Foo(std::vector<T> const& values) : vals( values ) {}
    const T &get(int i, int j) const {
        std::cout << "Read" << std::endl;
        return vals[i];
    }
    void put(int i, int j, T const& newValue ) {
        std::cout << "Write" << std::endl;
        vals[i] = newValue;
    } 
    T const& operator()(int i, int j) const {
        std::cout << "Read" << std::endl;
        return vals[i];
    }
    Proxy operator()(int i, int j) {
        return Proxy( this, i, j );
    } 

private:
    std::vector<T> vals;
};

int
main()
{
    std::vector<int> values(100, 1);

    Foo<int> f{values};
    f(1,1) = 42; //here it writes:OK
    std::cout << f(1,1) << std::endl;
    return 0;
}

有点啰嗦,但这是一个标准的成语,而且这个类基本上是样板的。Proxy

评论

0赞 cnewbie 11/1/2023
谢谢你的提示!我知道 Proxy 模式,但我不太喜欢我必须在每次调用运算符时构造一个 Proxy 对象的事实。这难道不是密集型计算代码的问题吗?@JamesKanze()
0赞 cnewbie 11/1/2023
或者您是否使用 RVO 实际上适用于运营商的事实?@JamesKanze()
0赞 James Kanze 11/2/2023
@cnewbie我真的指望代理类是完全内联的,编译器应该完全优化它(具有足够高的优化水平 - 在没有优化的构建中,您仍然会看到它在调试器中构造)。
0赞 James Kanze 11/2/2023
换句话说,如果你编写 ,编译器应该生成与你编写 相同的代码。这种优化是相当标准的,并且已经持续了 30 或 40 年。f( 1, 2 ) = 42f.put( 1, 2, 42 )