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
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
>:
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):