假设我有下面的类X,我想返回一个内部成员的访问:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

两个成员函数X::Z()和X::Z() const在大括号内具有相同的代码。这是重复的代码,可能会导致具有复杂逻辑的长函数的维护问题。

有办法避免这种代码重复吗?


当前回答

令我惊讶的是,有这么多不同的答案,但几乎所有的答案都依赖于沉重的模板魔法。模板功能强大,但有时宏在简洁方面胜过模板。最大的通用性通常通过两者结合来实现。

我写了一个宏FROM_CONST_OVERLOAD(),它可以放在非const函数中调用const函数。

使用示例:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

简单且可重用的实现:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

解释:

在许多回答中,避免在非const成员函数中代码重复的典型模式是:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

使用类型推断可以避免很多这种样板文件。首先,const_cast可以封装在WithoutConst()中,它推断其参数的类型并删除const限定符。其次,可以在WithConst()中使用类似的方法对this指针进行const限定,从而可以调用const重载方法。

剩下的是一个简单的宏,它在调用前加上正确限定的this->,并从结果中删除const。由于宏中使用的表达式几乎总是一个简单的带有1:1转发参数的函数调用,因此宏的缺点(如多重求值)并没有发挥作用。省略号和__VA_ARGS__也可以使用,但不应该被需要,因为逗号(作为参数分隔符)出现在括号内。

这种方法有几个好处:

最小和自然的语法——只需将调用包装在FROM_CONST_OVERLOAD()中。 不需要额外的成员函数 兼容c++ 98 简单的实现,没有模板元编程和零依赖 可扩展:可以添加其他const关系(如const_iterator、std::shared_ptr<const T>等)。为此,只需重载对应类型的WithoutConst()。

限制:此解决方案针对非const重载与const重载完全相同的场景进行了优化,因此参数可以1:1转发。如果你的逻辑不同,并且你没有通过this->方法(args)调用const版本,你可以考虑其他方法。

其他回答

有关详细说明,请参阅《Effective c++》第23页第3项“尽可能使用const”中的标题“避免const和非const成员函数中的重复”,由Scott Meyers 3d编辑,ISBN-13: 9780321334879。

以下是Meyers的解决方案(简化版):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

这两个类型转换和函数调用可能很难看,但在非const方法中是正确的,因为这意味着对象一开始就不是const对象。(Meyers对此进行了深入的讨论。)

c++ 23更新了这个问题的最佳答案,因为推导出了这个:

struct s {
    auto && f(this auto && self) {
        // all the common code goes here
    }
};

单个函数模板可作为普通成员函数调用,并为您推导正确的引用类型。没有错误的类型转换,没有为一个概念上的东西编写多个函数。

令我惊讶的是,有这么多不同的答案,但几乎所有的答案都依赖于沉重的模板魔法。模板功能强大,但有时宏在简洁方面胜过模板。最大的通用性通常通过两者结合来实现。

我写了一个宏FROM_CONST_OVERLOAD(),它可以放在非const函数中调用const函数。

使用示例:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

简单且可重用的实现:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

解释:

在许多回答中,避免在非const成员函数中代码重复的典型模式是:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

使用类型推断可以避免很多这种样板文件。首先,const_cast可以封装在WithoutConst()中,它推断其参数的类型并删除const限定符。其次,可以在WithConst()中使用类似的方法对this指针进行const限定,从而可以调用const重载方法。

剩下的是一个简单的宏,它在调用前加上正确限定的this->,并从结果中删除const。由于宏中使用的表达式几乎总是一个简单的带有1:1转发参数的函数调用,因此宏的缺点(如多重求值)并没有发挥作用。省略号和__VA_ARGS__也可以使用,但不应该被需要,因为逗号(作为参数分隔符)出现在括号内。

这种方法有几个好处:

最小和自然的语法——只需将调用包装在FROM_CONST_OVERLOAD()中。 不需要额外的成员函数 兼容c++ 98 简单的实现,没有模板元编程和零依赖 可扩展:可以添加其他const关系(如const_iterator、std::shared_ptr<const T>等)。为此,只需重载对应类型的WithoutConst()。

限制:此解决方案针对非const重载与const重载完全相同的场景进行了优化,因此参数可以1:1转发。如果你的逻辑不同,并且你没有通过this->方法(args)调用const版本,你可以考虑其他方法。

虽然这里的大多数答案都建议使用const_cast, CppCoreGuidelines有一个章节是关于这个的:

相反,更喜欢共享实现。通常,你可以让非const函数调用const函数。然而,当存在复杂的逻辑时,这可能导致以下模式,仍然诉诸于const_cast:

class Foo {
public:
    // not great, non-const calls const version but resorts to const_cast
    Bar& get_bar()
    {
        return const_cast<Bar&>(static_cast<const Foo&>(*this).get_bar());
    }
    const Bar& get_bar() const
    {
        /* the complex logic around getting a const reference to my_bar */
    }
private:
    Bar my_bar;
};

尽管这种模式在正确应用时是安全的,因为 调用者必须一开始就有一个非const对象,这并不理想 因为安全很难作为检查规则自动执行。 相反,更倾向于将公共代码放在公共helper函数中—— 让它成为一个模板,这样它就可以推导出const。这个不需要任何东西 Const_cast:

class Foo {
public:                         // good
          Bar& get_bar()       { return get_bar_impl(*this); }
    const Bar& get_bar() const { return get_bar_impl(*this); }
private:
    Bar my_bar;

    template<class T>           // good, deduces whether T is const or non-const
    static auto& get_bar_impl(T& t)
        { /* the complex logic around getting a possibly-const reference to my_bar */ }
};

注意:不要在模板中做大量不依赖的工作,这会导致代码膨胀。例如,如果get_bar_impl的全部或部分可以是非依赖的,并分解成一个公共的非模板函数,则可以进一步改进,从而可能大大减少代码大小。

如果你不喜欢const强制转换,我使用这个c++ 17版本的模板静态帮助器函数,由另一个答案建议,并带有可选的SFINAE测试。

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

完整版:https://godbolt.org/z/mMK4r3