如何让编译器更喜欢 C++ 中的 const 方法重载?

How to get compiler to prefer const method overloading in C++?

我有一个 class 的 C++ 成员函数,带有 const 和非 const 重载。

Class Example {
public:
  int const & Access() const;
  int       & Access();

  [...]
};

我希望 const 版本成为首选,因为在我的代码中性能要好得多,非 const 版本会导致创建底层共享对象的副本以允许修改。

现在,如果我的调用者有一个非常量 Example 对象,即使未修改结果 int,也会使用非常量 Access() 方法。

Example example;

if ( modify ) {
    example.Access() = 23;    // Need to perform expensive copy here.
} else {
    cout << example.Access(); // No need to copy, read-only access.
}

有没有一种方法,例如区分 return 值的左值和右值使用,也许使用模板的完美转发,在 C++17 中创建一个类似的机制,允许调用者拥有一个如果 return 值被修改,编译器只使用非常量版本的语法?

我需要它的另一个例子是 operator -> (),我有一个 const 和非 const 版本的运算符。当调用 const 的方法时,我希望编译器更喜欢 operator -> ().

const 版本
Class Shared {
  public:
    int Read() const;
    void Write(int value);

    [...]
};

template <typename BaseClass>
class Callback {
public:
  BaseClass const * operator -> () const; // No tracking needed, read-only access.
  BaseClass       * operator -> ();       // Track possible modification.

  [...]
};

typedef Callback<Shared> SharedHandle;

Shared shared;
SharedHandle sharedHandle(&shared);

if ( modify ) {
  sharedHandle->write(23);
} else {
  cout << sharedHandle->Read();
}

您可以使用非常量版本 return 代理对象,根据其用途确定是否需要修改。

class Example {
private:
    class AccessProxy {
        friend Example;
    public:
        AccessProxy(AccessProxy const &) = delete;

        operator int const & () const &&
        { return std::as_const(*m_example).Access(); }

        operator int const & operator= (int value) && {
            m_example->assign(value);
            return *this;
        }

        operator int const & operator= (AccessProxy const & rhs) && {
            m_example->assign(rhs);
            return *this;
        }

    private:
        explicit AccessProxy(Example & example) : m_example(&example) {}
        Example * const m_example;
    };

public:
  int const & Access() const;
  AccessProxy Access() { return AccessProxy(*this); }

  // ...

private:
  void assign(int value);
};

这不允许直接在代理上修改运算符,例如 ++example.Access()example.Access *= 3,但也可以添加这些运算符。

请注意,这并不完全等同于原始版本。显然,您不能将 int& 引用绑定到 example.Access() 表达式。在涉及用户定义的转换之前工作的代码现在可能无法编译,因为它需要两个用户定义的转换,因此可能存在差异。最棘手的区别是在代码中

auto && thing = example.Access();

thing 的类型现在是隐藏类型 Example::AccessProxy。您可以记录不这样做,但通过完美转发将其传递给函数模板可能会导致一些危险或意外的行为。删除复制构造函数并使其他 public 成员需要右值是为了阻止大多数意外错误使用代理类型,但它并不完全完美。

最简单的方法是创建一个 CAccess 成员(类似于 stdlib 容器上的 cbegin):

class Example {
public:
  int const & Access() const;
  int       & Access();
  int const & CAccess() const { return Access(); }
  // No non-const CAccess, so always calls `int const& Access() const`
};

这有一个缺点,如果你不修改它,你需要记得调用 CAccess

您也可以 return 代理代替:

class Example;

class AccessProxy {
  Example& e;
  explicit AccessProxy(Example& e_) noexcept : e(e_) {}
  friend class Example;
public:
  operator int const&() const;
  int& operator=(int) const;
};

class Example {
public:
  int const & Access() const;
  AccessProxy Access() {
      return { *this };
  }
private:
  int & ActuallyAccess();
  friend class AccessProxy;
};

inline AccessProxy::operator int const&() const {
    return e.Access();
}

inline int& AccessProxy::operator=(int v) const {
    int& value = e.ActuallyAccess();
    value = v;
    return value;
};

但这里的缺点是类型不再是int&,这可能会导致一些问题,只有operator=被重载。

第二个可以轻松应用于 operators,通过制作模板 class,如下所示:

#include <utility>

template<class T, class Class, T&(Class::* GetMutable)(), T const&(Class::* GetImmutable)() const>
class AccessProxy {
  Class& e;
  T& getMutable() const {
      return (e.*GetMutable)();
  }
  const T& getImmutable() const {
      return (e.*GetImmutable)();
  }
public:
  explicit AccessProxy(Class& e_) noexcept : e(e_) {}
  operator T const&() const {
      return getImmutable();
  }
  template<class U>
  decltype(auto) operator=(U&& arg) const {
      return (getMutable() = std::forward<U>(arg));
  }
};

class Example {
public:
  int const & Access() const;
  auto Access() {
      return AccessProxy<int, Example, &Example::ActuallyAccess, &Example::Access>{ *this };
  }
private:
  int & ActuallyAccess();
};

(虽然 AccessProxy::operator-> 也需要定义)

并且第一种方法不适用于 operator 成员(除非您愿意将 sharedHandle->read() 更改为 sharedHandle.CGet().read()

这可能不是问题的 100% 匹配,但您可以创建一个方便的模板函数,而不是修改许多存在此问题的 类:

template <typename T>
inline T const& cnst(T& a)
{
    return a;
}

然后可以这样使用:

Example example;

if ( modify ) {
    example.Access() = 23;    // Need to perform expensive copy here.
} else {
    cout << cnst(example).Access(); // No need to copy, read-only access.
}

这样做的好处是,您可以完全控制何时显式仅使用 const 访问,但如果由于某种原因您的对象已经是 const,它仍然可以工作(因为 const const& 仍然是 const& ).

缺点当然是您需要手动将其放入您的代码中,同时您希望编译器能够检测到它。

这当然本质上是一个 const_cast,但它更好用。