Aliasing constructed shared_ptr as key of map or set

Question | Mar 17, 2020 | hkumar 

The Concept

Two std::shared_ptr instances can be compared with the defined relational operators for the std::shared_ptr<T> class (through the operator <=> since C++20). Therefore, std::shared_ptr can be used as keys in associative containers such as std::map, std::unordered_map, std::set, and std::unordered_set:

//Default comparison with std::less (<)
std::map<std::shared_ptr<std::string>, std::string> amap;

auto sps = std::make_shared<std::string>("Hello");

amap[sps] = "World"; //OK

Note that the shared_ptr relational operators compare only the held raw pointers (the pointer returned by the get() method). So if two shared_ptrs are holding the same raw pointer, they would compare equal:

auto ip1 = std::make_shared<int>(10);
auto ip2 = ip1;

std::cout << (ip1 < ip2 ? "True" : "False"); //Always False
std::cout << (ip1 == ip2 ? "True" : "False"); //Always True

However, a shared_ptr can hold a raw pointer that is different than the pointer to the managed object, which gets deleted when the reference count reaches zero. This use case of shared_ptr is quite peculiar and is done through the aliasing constructor of std::shared_ptr<T>:

template <class Y>
shared_ptr (const shared_ptr<Y>& r, T* ptr) noexcept;

The aliasing constructor takes a shared_ptr (r above) with which the object ownership is shared, and an unrelated pointer (ptr above) that is held as the raw pointer and returned by the get() method. Therefore, a shared_ptr is capable of pointing at one object and managing the other. The common use of aliasing constructor is in creating a shared_ptr that holds a raw pointer to a part or member of an object but still manages the enclosing object. Here is an example:

struct Part {
 //..
};

struct Whole {
 Part p1, p2;
 //...
};

std::shared_ptr<Part> foo() {
 auto wp = std::make_shared<Whole>();

 //Return std::shared_ptr<Part> using aliasing
 return std::shared_ptr<Part>(wp, &wp->p1);

 //The Whole object is not deleted on return
}

//call foo in a function
void bar() {
 /*pp is a shared_ptr<Part> but manages a Whole object*/ 
 auto pp = foo();
 //'Whole' object is disposed of when this block ends
}

In those cases where the held raw pointer is different than the managed pointer, it could be desirable to compare the managed pointers instead of the raw pointer. The std::owner_less function object essentially compares the managed pointers and can be used as a comparator with associative containers in place of the default std::less comparator. The std::owner_less merely calls the owner_before method of shared_ptr. Here is the comparison between std::less and std::owner_less:

//Create an alias for readability
using PartPtr = std::shared_ptr<Part>;

auto wp = std::make_shared<Whole>();

//Create std::shared_ptr<Part> using aliasing
auto pp1 = PartPtr(wp, &wp->p1);
auto pp2 = PartPtr(wp, &wp->p2);

/*Shows that pp1 and pp2 are compared as equal 
   using std::owner_less but not with std::less*/
std::cout << std::boolalpha
 << std::less<PartPtr>()(pp1, pp2) //true 
 << std::less<PartPtr>()(pp2, pp1) //false 
 << std::owner_less<PartPtr>()(pp1, pp2) //false
 << std::owner_less<PartPtr>()(pp2, pp1); //false

//create a set with default std::less comparator
std::set<PartPtr> pset;
//create a set with std::owner_less comparator
std::set<PartPtr,std::owner_less<PartPtr>> pownerset;

pset.insert(pp1); //inserts
pset.insert(pp2); //inserts

pownerset.insert(pp1); //inserts
pownerset.insert(pp2); //returns existing

std::cout << pset.size(); //2
std::cout << pownerset.size(); //1

An Example and A Question

Let's look at a more interesting example of using the aliasing constructor. In a financial application, we have an Asset class and a Basket class that contains a collection of Assets. Given a list of asset tickers (symbols e.g., IBM), the Basket class constructs and initializes its assets:

struct Asset {
 std::string ticker;
 //...more fields
};

struct Basket {
 Basket(const std::vector<std::string>& tickers) {
  //Load data from database and create assets
  for(auto& t : tickers)
   assets.push_back({t /*,more fields*/});
 }
 std::vector<Asset> assets;
};

A function getBasketAssets (shown below) can fill a list of shared_ptr<Asset> for a given list of tickers. First, a Basket object is initialized that loads all the required assets. The Basket object is managed by a shared_ptr<Basket>. Then, multiple shared_ptr<Asset> instances are created using aliasing constructor from the shared_ptr<Basket> and pushed into a given list, as shown below:

void getBasketAssets(const std::vector<std::string>& tickers,
           std::vector<std::shared_ptr<Asset>>& assets) {

 //Create a basket that loads assets
 auto bp = std::make_shared<Basket>(tickers);

 for(auto& a : bp->assets) {
  //Aliasing constructor
  assets.push_back(std::shared_ptr<Asset>(bp, &a));
 }
}

Note that the Basket object is not disposed of when the getBasketAssets returns and the shared_ptr<Basket> is destroyed. The Basket object is deleted only when all the shared_ptr<Asset> are destroyed and the reference count reaches zero. The following illustration shows the relationship between the shared_ptr<Asset> and shared_ptr<Basket>:

shared_ptr Basket and Asset

We call getBasketAssets a few times for different ticker lists and collect all the shared_ptr<Asset> instances in one vector<shared_ptr<Asset>>:

std::vector<std::shared_ptr<Asset>> allAssets;

getBasketAssets({"KO","PEP"},allAssets); // 2 Assets
getBasketAssets({"MSFT","AAPL","FB","AMZN"},allAssets); // 4 Assets
getBasketAssets({"SBUX","MCD","CMG"},allAssets); // 3 Assets

Suppose we want to get the counts of Asset objects from each Basket and do so by incrementing a counter for each shared_ptr<Asset> in a map, as shown:

using Comparator = _______; //Intentionally Omitted

std::map<std::shared_ptr<Asset>, size_t, Comparator> countMap;

for(auto& ap : allAssets)
 countMap[ap]++;

for(auto& kvp : countMap)
 std::cout << kvp.second << " ";

The output of the above code depends on the Comparator we choose for the map, whose definition is intentionally omitted. As we created the three Baskets of 2,4 and 3 Assets above, we expect the above code to print "2 4 3" in no particular order. The code should not print all 1s ("1 1 1 1 1 1 1 1 1").

Select below a function object that satisfies our requirement as the Comparator (Check Explanations for details):