如何让编译器在C++中更喜欢常量方法重载?

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

本文关键字:常量 更喜欢 方法 重载 C++ 编译器      更新时间:2023-10-16

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

Class Example {
public:
int const & Access() const;
int       & Access();
[...]
};

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

现在,如果我的调用者有一个非 constExample对象,即使结果 int 没有被修改,也会使用非 const 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.
}

有没有办法,例如区分返回值的左值和右值用法,也许使用模板的完美转发,在 C++17 中创建类似的机制,允许调用方有一种语法,编译器仅在修改返回值时使用非 const 版本?

我需要它的另一个例子是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();
}

最简单的方法是创建一个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

您也可以改为返回代理:

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=是重载的。

第二个可以很容易地应用于operator,通过创建一个模板类,如下所示:

#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()(

您可以让非 const 版本返回一个代理对象,该对象根据其使用情况确定是否需要修改。

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。您可以记录不这样做,但是通过完美转发到函数模板传递它可能会遇到一些危险或意外的行为。删除复制构造函数并使其他公共成员需要右值是尝试阻止代理类型的大多数意外错误使用,但它并不完全完美。

这可能不是问题的 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 access,但是如果由于某种原因您的对象已经是const,它仍然可以工作(因为const const&仍然是const&

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

这当然本质上是一种const_cast,但使用起来要好得多。

相关文章: