Last Update:
How to Share Code with Const and Non-Const Functions in C++
Table of Contents
During the development of a container-like type, I run into the problem of how to share code between a const
and non-const
member functions. In this article, I’d like to explain what are the issues and possible solutions. We can even go on a bleeding edge and apply some C++20 features. Which technique is most friendly?
Have a look.
The Problem
The container I’m working on is more complicated, but here’s a simplified version to illustrate the problem:
struct Part {
std::string _name;
bool _isAvailable { false };
};
class PartsStore {
public:
PartsStore(const std::map<int, Part>& parts) : _parts(parts) { }
bool Contains(std::string_view name) {
return FindByNameImpl(name) != nullptr;
}
void SetAvailability(std::string_view name, bool isAvailable) {
auto pPart = FindByNameImpl(name);
if (pPart)
pPart->_isAvailable = isAvailable;
}
private:
Part* FindByNameImpl(std::string_view name) {
auto it = std::find_if(begin(_parts), end(_parts),
[&name](const auto& entry) {
return entry.second._name == name;
}
);
if (it != _parts.end())
return &it->second;
return nullptr;
}
std::map<int, Part> _parts;
};
Code available here @Wandbox
As you can see above, we have a container of Parts. This class type wraps a standard std::map
and adds some additional interface.
The core issue is that there are member functions like Contains
or SetAvailability
. Right now they are all non-const
and then call some internal helper function that does the job of finding a Part by name.
FindByNameImpl
is maybe not super advanced, but we can assume that such a function can contain some extra logic that we’d like to share across other callers.
What’s the issue then? PartsStore
seems to do the job.
The Contains
function is non-const
… but it should (not to mention noexcept
, but we can save that for some other discussion). It doesn’t modify the internal state, so we really have to apply some const correctness here.
See more reasons about applying const
in my separate article: Bartek’s coding blog: Please declare your variables as const
But then, the code won’t compile as this function calls non-const
code. Also, we cannot just mark FindByNameImpl
with const
as it’s called by non-const
function SetAvailability
(this won’t compile without explicit casts). So they are all “connected”…
That’s why it would be best to find an approach and share the code in FindByNameImpl
efficiently between those two functions.
Sharing Code in Const and Non-Const Functions
I did some research and found several ways of how we can address this “sharing” problem.
Let’s start with the most obvious one:
Code Duplication
While this simple solution is probably not the best approach it allows us to see where const
has to be applied:
Part* FindByNameImpl(std::string_view name) {
auto it = std::find_if(begin(_parts), end(_parts),
[&name](const auto& entry) {
return entry.second._name == name;
}
);
if (it != _parts.end())
return &it->second;
return nullptr;
}
const Part* FindByNameImpl(std::string_view name) const {
auto it = std::find_if(begin(_parts), end(_parts),
[&name](const auto& entry) {
return entry.second._name == name;
}
);
if (it != _parts.end())
return &it->second;
return nullptr;
}
See code @Wandbox
The Mutable Keyword
We had code duplication in the previous point, so why not take other direction and use a handy brute force approach and apply mutable
to our data member?
Just to remind:
mutable
- permits modification of the class member declared mutable even if the containing object is declared const.
But… this is an even worse idea than a direct code duplication!
See in the C++ Core Guidelines: ES 50
Sometimes, “cast away const” is to allow the updating of some transient information of an otherwise immutable object. Examples are caching, memoization, and precomputation. Such examples are often handled as well or better using mutable or an indirection than with a
const_cast
.
In other words, sometimes it might be handy to apply mutable
but only to additional data members that “enhances” operations on the core state of our class. For example, we can have some extra caching system.
In our case std::map<int, Part> _parts;
is “core” state, so it’s definitely not the best idea to alter it.
const_cast
From non-const Function
Finally, we can look at some more concrete solution.
Let’s reach out to Scott Meyers and in his Effective C++ 3rd Edition. On page 23, Item 3 (on using const
) we can read that a non const
function can safely call const
one. To achieve this, we can leverage <const_cast>
. In our case, this boils down to the following code:
class PartsStore {
public:
PartsStore(const std::map<int, Part>& parts) : _parts(parts) { }
bool Contains(std::string_view name) const {
return FindByNameImpl(name) != nullptr;
}
void SetAvailability(std::string_view name, bool isAvailable) {
auto pPart = const_cast<Part*>(FindByNameImpl(name));
if (pPart)
pPart->_isAvailable = isAvailable;
}
private:
const Part* FindByNameImpl(std::string_view name) const {
// impl...
}
std::map<int, Part> _parts;
};
See code @Wandbox
In this case, I removed const
from the pointer that is returned from FindByNameImpl
which is now a constant function.
There might be many variations on that approach, especially when you want to avoid recursive calls…
As Scott Meyers explains, calling functions this way is safe. Since a const
function promises not to modify the internal state of the object, then we’re not breaking it. On the other hand, the reverse is not possible - i.e. calling non-const
function from a const
one. This time we break a promise of not altering the state, so this can generate Undefined Behavior (UB).
This technique is very promising, but let’s see another one, that doesn’t require casts at all.
Templates To The Rescue
In a basic form, we can use templates to generate necessary code, depending on the caller needs. In other words, the compiler will generate two versions of the function for us.
For the implementation I created a static function template. The function is parametrised over the container
type:
template <typename T>
static auto FindByNameImpl(std::string_view name, T& container) {
auto it = std::find_if(begin(container), end(container),
[&name](const auto& entry) {
return entry.second._name == name;
}
);
return it != end(container) ? &it->second : nullptr;
}
See code @Wandbox
This is nice! The compiler can enforce additional checks and don’t need any casts. One disadvantage is that we have a function template, so possible we need to put that in a header file… or define it a free function in a cpp file.
Enhancing with C++20
We can even experiment with some C++20 features and restrict out function template to work only with the map container:
template <typename T>
requires std::is_same_v<std::map<int, Part>, std::remove_cv_t<T>>
static auto FindByNameImpl(std::string_view name, T& container) {
// code...
}
See code @Wandbox
Summary
In the article, you’ve seen four techniques (plus one enhancement) that allows you to share code between const
and non-const
member functions. While the first two patterns are probably not the best idea: direct code duplication and the application of the mutable
keyword - they server the illustrative purpose. But the last two techniques are more practical and safer.
For my use case, I think I’ll stick with a template solution as it doesn’t need any casts and compiler can check const
correctness better.
What do you think about those solutions? Maybe there are some other approaches?
References
- Effective C++: 55 Specific Ways to Improve Your Programs and Designs 3rd Edition
- Mutable for const-correctness - Simplify C++!
Join the discussion @reddit/r/cpp.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: