Created
October 6, 2018 21:02
-
-
Save arrieta/96ad0b6b46ab08b8fe03eb802f9b4378 to your computer and use it in GitHub Desktop.
C++ std::shared_ptr<T> Aliasing Constructor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// About std::shared_ptr<T> aliasing constructor. | |
// J. Arrieta, Nabla Zero Labs. | |
// October 06, 2018. | |
// | |
// The std::shared_ptr<T> aliasing constructor has the signature | |
// | |
// template< class Y > | |
// shared_ptr(const shared_ptr<Y>& r, element_type* ptr) noexcept; | |
// | |
// According to cppreference.com, it "constructs a shared_ptr which shares | |
// ownership information with r, but holds an unrelated and unmanaged pointer | |
// ptr. If this shared_ptr is the last of the group to go out of scope, it will | |
// call the stored deleter for the object originally managed by r. However, | |
// calling get() on this shared_ptr will always return a copy of ptr. It is the | |
// responsibility of the programmer to make sure that this ptr remains valid as | |
// long as this shared_ptr exists, such as in the typical use cases where ptr is | |
// a member of the object managed by r or is an alias (e.g., downcast) of | |
// r.get()". | |
// | |
// There are many use cases that can take advantage of the aliasing constructor, | |
// but the one that immediately comes to mind is refering to a member of a data | |
// structure while hiding the data management aspects. | |
// | |
// For example: a linked list can be implemented in terms of a Node<T> structure | |
// that contains pointers to other Node<T> elements. However, the user does not | |
// need to know about these pointers, as the only thing she cares about is the | |
// actual T. | |
// | |
// In this case, the Node<T> can be managed by shared_ptr<Node<T>>, and the | |
// access returns a shared_ptr<T>. | |
// | |
// The example below is NOT meant to illustrate how to implement a good linked | |
// list. Instead, it is meant as a quick demonstration of the concept: control | |
// structure and lifetime via internal nodes and shared_ptr, while providing an | |
// API that only exposes the data. | |
#include <iostream> | |
#include <memory> | |
template <typename T> | |
struct List { | |
// This is the internal control structure, an implementation detail that | |
// should not leak to the API. | |
struct Node { | |
T data; | |
std::shared_ptr<Node> next = nullptr; | |
}; | |
// Initialize an empty list | |
List() : head(std::make_shared<Node>()) {} | |
// Let's imagine T is expensive and we only provide facilities to move it into | |
// the container. | |
void insert(T&& value) { | |
auto next = head; | |
while (next->next) { | |
next = next->next; | |
} | |
next->next = std::make_shared<Node>(); | |
next->next->data = std::move(value); | |
} | |
// Get a handle to the data stored at `index`. Notice how the shared pointer | |
// references a `const T` and not a `Node<T>`. The `Node` control structure is | |
// not part of the API. | |
std::shared_ptr<const T> at(std::size_t index) const { | |
std::size_t current = 0; | |
auto next = head; | |
while (next->next) { | |
if (current++ == index) { | |
// Return a handle to the data contained in the appropriate node, while | |
// controling lifetime via the shared_pointer of the underlying node. | |
auto&& underlying_node = next->next; | |
auto&& pointer_to_data = &underlying_node->data; | |
// aliasing constructor | |
return {underlying_node, pointer_to_data}; | |
} | |
next = next->next; | |
} | |
return nullptr; | |
} | |
std::shared_ptr<Node> head; | |
}; | |
// An imaginary expensive object. | |
struct Expensive { | |
void report() const { std::cout << "Expensive @" << this << "\n"; } | |
}; | |
int main() { | |
// The user can create a list of "expensive" objects. | |
List<Expensive> list; | |
list.insert(Expensive()); | |
list.insert(Expensive()); | |
list.insert(Expensive()); | |
// The handle is a shared_ptr<const Expensive> whose lifetime will continue | |
// even if `list` goes out of scope. | |
auto handle = list.at(2); | |
// As far as the user is concerned, `handle` is not at all related to a list, | |
// and there is no mention of a Node<T> class or a Node<Expensive> object. It | |
// is simply a shared_ptr to an Expensive object. | |
handle->report(); | |
// Some stupid traversal. | |
std::size_t k = 0; | |
while (true) { | |
auto thing = list.at(k); | |
if (thing) { | |
std::cout << "Reporting on thing " << k << "\n"; | |
thing->report(); | |
++k; | |
} else { | |
break; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Possible output: