Home > Blockchain >  What is the correct pattern to define a class that accepts a const and non-const object without viol
What is the correct pattern to define a class that accepts a const and non-const object without viol

Time:10-21

There are a number of obvious problems with this class and it is meant to illustrate the problem rather than demonstrate working code.

#include <list>

template<typename T>
class OnlyLists {
private:
   const std::list<T>* p_list;
   
public:
   OnlyLists(std::list<T>* list) : p_list(list) { }
   OnlyLists(const std::list<T>* list) : p_list(list) { }
   
   void modifiesList() {
      const_cast<std::list<T>*>(p_list)->clear();
   }
   
   size_t viewsList() const {
      return p_list->size();
   }
};

int main() {
   const std::list<int> const_list;
   
   // Constness is preserved
   const OnlyLists<int> a(&const_list);
   
   // Constness is discarded
   OnlyLists<int> b(&const_list);
   
   // Constness is discarded (another inherited problem)
   b = a;
   
   return 0;
}

I want to create a class that can view and modify a particular object (a list in this example). Its methods are exposed such that only the non-const ones can modify the list. The problem is that this constness on the methods only relates to whether the OnlyLists object is const. And if I accept a const list<T>* without also declaring the OnlyLists object as const then I can modify the list when I shouldn't.

It's possible to work around this by making the constructors private and making object creation a static method to ensure constness but this approach throws away public constructors entirely and is instead an anti-pattern. Additionally, keeping only one pointer p_list would already requires constness to be cast away when calling any method that would modify the list.

What is the correct pattern in C where I want one class to be able to view and modify another object like this?

The only solution that I can see is that any class which accepts both const and non-const types can only provide a const view. But the only issue here is with the object construction; the actual interface already protects the object from modification.

I thought about moving the entire list type into the template but this seems redundant if the class already accepts only lists. If I move the entire container into the template then constraints would need to be added to ensure that it is a list, etc. This seems overly complex when the constructor already limits itself to accepting only lists.

CodePudding user response:

Since OnlyLists has reference semantics in the same way as std::unique_ptr, the usual practice is to control mutation using the constness of the template parameter:

  • OnlyLists<T> can store a pointer to std::list<T>, and
  • OnlyLists<const T> can accept a pointer to either std::list<T> or const std::list<T>, but will always store a pointer to const std::list<T>, and permit only const operations on the list.

The modifiesList() method might not be defined for OnlyLists<const T>, or it might be defined but cause a compilation error if instantiated (used).

Example:

template<typename T>
class OnlyLists {
private:
   std::list<T>* p_list;
   
public:
   OnlyLists(std::list<T>* list) : p_list(list) { }
   
   void modifiesList() {
      p_list->clear();
   }
   
   size_t viewsList() const {
      return p_list->size();
   }
};

// This is a partial specialization for the case where T is const
template<typename T>
class OnlyLists<const T> {
private:
   std::list<T> const* p_list;
   
public:
   OnlyLists(std::list<T> const* list) : p_list(list) { }
   
   size_t viewsList() const {
      return p_list->size();
   }
};

The issue of how to avoid duplication of viewsList is left as an exercise for the reader.

CodePudding user response:

C 17 solution:

If you're willing to modify your syntax a bit, you could add a bool modifiable template parameter to OnlyLists, create a partial specification for modifiable = false and introcude class template argument deduction (CTAD) to deduce the value of modifiable given the constructor parameters:

template<typename T, bool modifiable>
class OnlyLists
{
public:
    using ListType = std::list<T>;

    OnlyLists(ListType* list) : p_list(list) { }

    void modifiesList() {
        p_list->clear();
    }

    size_t viewsList() const {
        return p_list->size();
    }

private:
    ListType* p_list;
};

template<typename T>
class OnlyLists<T, false>
{
public:
    using ListType = std::list<T> const;

    OnlyLists(ListType* list) : p_list(list) { }

    size_t viewsList() const {
        return p_list->size();
    }

private:
    ListType* p_list;
};

// modifiable should be true if and only if the type the pointer points to is non-const
template<typename ListType>
OnlyLists(ListType*) -> OnlyLists<typename ListType::value_type, !std::is_const_v<ListType>>;
int main()
{
    std::list<int> non_const_list;
    const std::list<int> const_list;

    auto a = OnlyLists(&const_list); // a has type OnlyLists<int, false>

    a.viewsList();
    a.modifiesList(); // compiler error

    // Constness is discarded
    auto b = OnlyLists(&non_const_list); // b has type OnlyLists<int, true>

    b.viewsList();
    b.modifiesList();

    a = b; // compiler error

    decltype(b) c(nullptr);
    c = b; // Ok. Uses auto-generated a copy assignment operator

    return 0;
}
  • Related