How do I remove code duplication between similar const and non-const member functions?

后端 未结 19 1671
天涯浪人
天涯浪人 2020-11-22 00:30

Let\'s say I have the following class X where I want to return access to an internal member:

class Z
{
    // details
};

class X
{
    std::vec         


        
相关标签:
19条回答
  • 2020-11-22 00:54

    Here's a C++17 version of the template static helper function, with and optional SFINAE test.

    #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); }
    };
    

    Full version: https://godbolt.org/z/mMK4r3

    0 讨论(0)
  • 2020-11-22 00:54

    I did this for a friend who rightfully justified the use of const_cast... not knowing about it I probably would have done something like this (not really elegant) :

    #include <iostream>
    
    class MyClass
    {
    
    public:
    
        int getI()
        {
            std::cout << "non-const getter" << std::endl;
            return privateGetI<MyClass, int>(*this);
        }
    
        const int getI() const
        {
            std::cout << "const getter" << std::endl;
            return privateGetI<const MyClass, const int>(*this);
        }
    
    private:
    
        template <class C, typename T>
        static T privateGetI(C c)
        {
            //do my stuff
            return c._i;
        }
    
        int _i;
    };
    
    int main()
    {
        const MyClass myConstClass = MyClass();
        myConstClass.getI();
    
        MyClass myNonConstClass;
        myNonConstClass.getI();
    
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-22 00:56

    You could also solve this with templates. This solution is slightly ugly (but the ugliness is hidden in the .cpp file) but it does provide compiler checking of constness, and no code duplication.

    .h file:

    #include <vector>
    
    class Z
    {
        // details
    };
    
    class X
    {
        std::vector<Z> vecZ;
    
    public:
        const std::vector<Z>& GetVector() const { return vecZ; }
        std::vector<Z>& GetVector() { return vecZ; }
    
        Z& GetZ( size_t index );
        const Z& GetZ( size_t index ) const;
    };
    

    .cpp file:

    #include "constnonconst.h"
    
    template< class ParentPtr, class Child >
    Child& GetZImpl( ParentPtr parent, size_t index )
    {
        // ... massive amounts of code ...
    
        // Note you may only use methods of X here that are
        // available in both const and non-const varieties.
    
        Child& ret = parent->GetVector()[index];
    
        // ... even more code ...
    
        return ret;
    }
    
    Z& X::GetZ( size_t index )
    {
        return GetZImpl< X*, Z >( this, index );
    }
    
    const Z& X::GetZ( size_t index ) const
    {
        return GetZImpl< const X*, const Z >( this, index );
    }
    

    The main disadvantage I can see is that because all the complex implementation of the method is in a global function, you either need to get hold of the members of X using public methods like GetVector() above (of which there always need to be a const and non-const version) or you could make this function a friend. But I don't like friends.

    [Edit: removed unneeded include of cstdio added during testing.]

    0 讨论(0)
  • 2020-11-22 00:56

    It's surprising to me that there are so many different answers, yet almost all rely on heavy template magic. Templates are powerful, but sometimes macros beat them in conciseness. Maximum versatility is often achieved by combining both.

    I wrote a macro FROM_CONST_OVERLOAD() which can be placed in the non-const function to invoke the const function.

    Example usage:

    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) );
        }
    };
    

    Simple and reusable implementation:

    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)
    

    Explanation:

    As posted in many answers, the typical pattern to avoid code duplication in a non-const member function is this:

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

    A lot of this boilerplate can be avoided using type inference. First, const_cast can be encapsulated in WithoutConst(), which infers the type of its argument and removes the const-qualifier. Second, a similar approach can be used in WithConst() to const-qualify the this pointer, which enables calling the const-overloaded method.

    The rest is a simple macro that prefixes the call with the correctly qualified this-> and removes const from the result. Since the expression used in the macro is almost always a simple function call with 1:1 forwarded arguments, drawbacks of macros such as multiple evaluation do not kick in. The ellipsis and __VA_ARGS__ could also be used, but should not be needed because commas (as argument separators) occur within parentheses.

    This approach has several benefits:

    • Minimal and natural syntax -- just wrap the call in FROM_CONST_OVERLOAD( )
    • No extra member function required
    • Compatible with C++98
    • Simple implementation, no template metaprogramming and zero dependencies
    • Extensible: other const relations can be added (like const_iterator, std::shared_ptr<const T>, etc.). For this, simply overload WithoutConst() for the corresponding types.

    Limitations: this solution is optimized for scenarios where the non-const overload is doing exactly the same as the const overload, so that arguments can be forwarded 1:1. If your logic differs and you are not calling the const version via this->Method(args), you may consider other approaches.

    0 讨论(0)
  • 2020-11-22 00:56

    For those (like me) who

    • use c++17
    • want to add the least amount of boilerplate/repetition and
    • don't mind using macros (while waiting for meta-classes...),

    here is another take:

    #include <utility>
    #include <type_traits>
    
    template <typename T> struct NonConst;
    template <typename T> struct NonConst<T const&> {using type = T&;};
    template <typename T> struct NonConst<T const*> {using type = T*;};
    
    #define NON_CONST(func)                                                     \
        template <typename... T> auto func(T&&... a)                            \
            -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
        {                                                                       \
            return const_cast<decltype(func(std::forward<T>(a)...))>(           \
                std::as_const(*this).func(std::forward<T>(a)...));              \
        }
    

    It is basically a mix of the answers from @Pait, @DavidStone and @sh1 (EDIT: and an improvement from @cdhowie). What it adds to the table is that you get away with only one extra line of code which simply names the function (but no argument or return type duplication):

    class X
    {
        const Z& get(size_t index) const { ... }
        NON_CONST(get)
    };
    

    Note: gcc fails to compile this prior to 8.1, clang-5 and upwards as well as MSVC-19 are happy (according to the compiler explorer).

    0 讨论(0)
  • 2020-11-22 00:58

    Is it cheating to use the preprocessor?

    struct A {
    
        #define GETTER_CORE_CODE       \
        /* line 1 of getter code */    \
        /* line 2 of getter code */    \
        /* .....etc............. */    \
        /* line n of getter code */       
    
        // ^ NOTE: line continuation char '\' on all lines but the last
    
       B& get() {
            GETTER_CORE_CODE
       }
    
       const B& get() const {
            GETTER_CORE_CODE
       }
    
       #undef GETTER_CORE_CODE
    
    };
    

    It's not as fancy as templates or casts, but it does make your intent ("these two functions are to be identical") pretty explicit.

    0 讨论(0)
提交回复
热议问题