Last Update:
One Trick with Private Names and Function Templates
Table of Contents
Last time in my blog post about How to Share Code with Const and Non-Const Functions in C++ I had a custom type declared and defined in one place (like in a header file). Recently, I tried to separate the declaration from implementation, and I got into a situation where one private function template was left.
In this article, I’d like to show you one trick that allowed me to convert this function template into a non-member function without giving up private details of the class.
How it Started
Here’s the initial code (simplified a bit):
class PartsStore {
// private nested type...
struct Part {
std::string name_;
bool isAvailable_ { false };
}
public:
PartsStore(const std::map<int, Part>& parts) : parts_(parts) { }
bool Contains(std::string_view name) const {
return FindByNameImpl(name, parts_) != nullptr;
}
void SetAvailability(std::string_view name, bool isAvailable) {
auto pPart = const_cast<Part*>(FindByNameImpl(name, parts_));
if (pPart)
pPart->isAvailable_ = isAvailable;
}
private:
template <typename T>
static auto FindByNameImpl(std::string_view name, T& container) {
// implementation...
}
std::map<int, Part> parts_;
};
PartsStore
operates on a map of nested structures Part
. We don’t want to expose this type outside, so it’s declared as private.
I had no problems with moving constructors, Contains
and SetAvailability
member functions.
But I also moved the template member function - FindByNameImpl
and extracted it as a non-member static function.
What’s the trick here?
Look at the converted function:
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;
}
It’s declared as a free, non-member template function, but it can access a private nested type! It works on a container of std::map<PartStore::Part>
.
During the template instantiation this function gets two versions:
- one for
std::map<PartStore::Part>
- and another for
const std::map<PartStore::Part>
On the other hand, if you tried to write a regular “explicit” function with those types:
static void FindTemp(std::map<int, PartsStore::Part>& container) { }
You’d get the following error:
prog.cc: In function 'void FindTemp(std::map<int, PartsStore::Part>&)':
prog.cc:14:24: error: 'struct PartsStore::Part' is private within this context
14 | void FindTemp(std::map<int, PartsStore::Part>& container) { }
It looks like we cannot use a name directly, but the compiler has no problem when creating instances of a function template.
Is that correct?
Read below.
Looking into the Standard
Initially, I thought that this might be a compiler error… lucky me! :) But after checking my example with three major compilers, I came to the conclusion that this is probably a well-defined technique and not an error.
Let’s try to find something in the Standard:
https://eel.is/c++draft/class.access#general-4
[Note 2: Because access control applies to names if access control is applied to a typedef name, only the accessibility of the typedef name itself is considered. The accessibility of the entity referred to by the typedef is not considered.
For example,
class A {
class B { };
public:
typedef B BB;
};
void f() {
A::BB x; // OK, typedef name A::BB is public
A::B y; // access error, A::B is private
}
>
> — *end note*]
And similarly you can write (thanks Andreas Fertig for the code sample!):
```cpp
class Test {
struct S { int i; }; // private
public:
S a; // expose S indirectly as variable a
};
int main() {
Test t{};
auto x = t.a; // capture the type of a
x.i = 4; // use a
}
You can “capture” the type in the above example, but you cannot use it explicitly. Later the code sample uses x.i
which is a public name and thus the compiler doesn’t report any issues.
This is also essential for lambdas:
auto GenLamba(int x) {
return [x]() { return x*x + 40; };
}
auto lambda = GenLambda(1);
lambda();
Since lambdas are “expanded” as a local function object class types, then we cannot “spell it out”. On the other hand, we know that the compiler generates a public call operator, that’s why there’s no issue executing it.
Summary
See the experimental code here: @Wandbox
I guess it’s a relatively rare situation. Still, when you have a function template in your class, you can try extracting it into a static non-member function and benefit from the access to private/protected details of the class (assuming the other names have public access).
The access control is applied on names, so while you cannot explicitly “say” a private, nested type, the compiler has no issues when using this in template instantiation. And as we’ve seen with a few examples, this ability is quite critical for many techniques: for example, returning a local structure, a local closure type, exposing a nested type…
I’m curious if you have more examples of such use cases.
I know that Jason Turner also had an episode on that recently, so you also can have a look: C++ Weekly - Ep 249 - Types That Shall Not Be Named - YouTube
Acknowledgements: Thanks to Tomasz Kamiński, Andreas Fertig, David Pilarski and My Patreon Group for valuable discussions on this topic.
Comments
Please join the discussion at this reddit/r/cpp thread.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: