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 tostd::list<T>
, andOnlyLists<const T>
can accept a pointer to eitherstd::list<T>
orconst std::list<T>
, but will always store a pointer toconst std::list<T>
, and permit onlyconst
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;
}