Avoiding code duplication with const / non-const member functions

793 views
Skip to first unread message

tmlen

unread,
Mar 18, 2015, 5:41:23 PM3/18/15
to std-pr...@isocpp.org
Container classes like std::vector  often provide pairs of member functions with almost the same functionality, one const and one non-const. In some cases these functions are trivial (ex. std::array::operator[]) but they can also be more complex functions, like std::map::equal_range.
There is often no "good" way to implement such functions:

(1) Manually writing two functions with (almost) identical code can be difficult to maintain if the code/algorithm is large:
class int_set {
private:
    
int* arr_;
    std
::size_t size_;

public:
    int& find(int value) {
        auto end = arr_ + size_;
        for(auto it = arr_; it != end; ++it)
            if(*it == value) return *it;
        throw std::invalid_argument("Not in set.");
    }

    const int& find(int value) const {
        auto end = arr_ + size_;
        for(auto it = arr_; it != end; ++it)
            if(*it == value) return *it;
        throw std::invalid_argument("Not in set.");
    }
};


(2) Avoiding the problem by defining a different interface:
class int_set {
private:
    
int* arr_;
    std
::size_t size_;

public:
    std::ptrdiff_t find(int value) const {
        auto end = arr_ + size_;
        for(auto it = arr_; it != end; ++it)
            if(*it == value) return (it - arr_);
        throw std::invalid_argument("Not in set.");
    }

    int& operator[](std::ptrdiff_t i) { return arr_[i]; }
   
 const int& operator[](std::ptrdiff_t i) const { return arr_[i]; }
};



(3) Another way is to make the calls forward to a templated member function, which gets instantiated in a const and non-const version. But this produces hard-to-read code and needs more complex constructs such as std::conditional :
class int_set {
private:
   
int* arr_;
    std
::size_t size_;

    template<typename Int_set>
   
static auto find_(Int_set& self, int value)
    -> std::conditional<std::is_const<Int_set>::value, const int&, int&> {
        auto end = self.arr_ + self.size_;
        for(auto it = self.arr_; it != end; ++it)
            if(*it == value) return *it;
        throw std::invalid_argument("Not in set.");
    }

public:
    int& find(int value) {
        return find_(*this, value);
    }

    const int& find(int value) const {
        return find_(*this, value);
    }
};

(4) Implementing one variant and using
const_cast for the other. May result in undefined behavior, and not always possible (for example when std::vector<int*> would need to become std::vector<const int*>).  But it has the advantage that the same binary code is not generated twice (similar to generics vs templates).

class int_set {
private:
    
int* arr_;
    std
::size_t size_;

public:
    int& find(int value) {
        const int& result = static_cast<const int_set&>(*this).find(value);
        return const_cast<int&>(result);
    }

    const int& find(int value) const {
        auto end = arr_ + size_;
        for(auto it = arr_; it != end; ++it)
            if(*it == value) return *it;
        throw std::invalid_argument("Not in set.");
    }
};




The underlying problem seems to be that container classes which own their elements need to enforce const-correctness, i.e. assure that when the caller has const access to the container, it can not get non-const access to its elements. (as for the STL containers, and automatically for public data members.)

View classes on the other hand always give const or non-const access, regardless if access to the view class is const. So there may be two variants of the view class, such as iterator and const_iterator. Or it depends on the constness of a template argument, like std::unique_ptr. (There can be a similar code duplication problem here.)

--

Maybe it would be useful to have an automated way to generate const and non-const member functions. For example using a new keyword autoconst (probably a bad choice since it is similar to auto const):

class int_set {
private:
    
int* arr_;
    std
::size_t size_;
public:
    autoconst int& find(int value) autoconst {
        auto end = arr_ + size_;
        for(auto it = arr_; it != end; ++it)
            if(*it == value) return *it;
        throw std::invalid_argument("Not in set.");
    }
};


The semantics of autoconst would be:

- If a member function is marked autoconst the compiler will generate two functions, from the one autoconst declaration and definition. One const and one non-const.
- autoconst can be used to qualify types in the declaration (return type and parameters), and types within the definition. In an autoconst function, autoconst T evaluates to T in the non-const version and to const T in the const version.

Even when the functions share the same code, their implementation may still be different: For example the with code 
auto begin_it = begin();
in an autoconst function, it can become an iterator in the non-const version, and a const_iterator in the const version. Because begin() is still manually defined in the two variants.

It may also be possible to define criteria for when the binary code generated from compiling the autoconst function versions will be exactly the same for both versions, so that the compiler can merge them as an optimization.

David Krauss

unread,
Mar 18, 2015, 7:56:01 PM3/18/15
to std-pr...@isocpp.org
On 2015–03–19, at 5:41 AM, tmlen <timle...@gmail.com> wrote:

Maybe it would be useful to have an automated way to generate const and non-const member functions. For example using a new keyword autoconst (probably a bad choice since it is similar to auto const):

I have a proposal in the works that uses export(const) for this purpose. This notation also supports preserving value category with export(const &&) and export(&&), and marks the function result as being an owned property of the object.


Separating that feature from the lifetime stuff, it would be nice to see auto(const), auto(const &&), and auto(&&) as well.

Also, it would be nice to substitute patterns in non-template function parameters. It opens the door to combinatorial explosion, but only if you put a lot of patterns in a row. And the explosion has already really happened, we just don’t have syntax (aside from macros) to deal with it elegantly.
Reply all
Reply to author
Forward
0 new messages