Transparent function objects

In Rust I tried to binary search a string slice in a string collection, and was surpriced that I had to resort from binary_search to either binary_search_by or binary_search_by_key:

fn main() {
    let vec: Vec<String> = Vec::new();
    // vec.binary_search("foo"); // doesn't compile, because a `String` is required
    vec.binary_search_by(|x| x.as_str().cmp("foo"));
    vec.binary_search_by_key(&"foo", |s| s.as_str());
}

In C++ the same scenario works just as I expect it to work - without any need to use a custom comparator:

#include <algorithm>
#include <vector>
#include <string>
using namespace std;
int main() {
    vector<string> const els{"a", "b", "c"};
    lower_bound(els.begin(), els.end(), "b"sv);
}

Then I decided to remember and summarize what I know about searching string slices in C++ string sets and compare it to Rust.

The example below presents three ways to declare a set of strings, the first of which have a hidden allocation in find. The second and third declarations are the ways to overcome the pitfall.

#include <map>
#include <string_view>
#include <string>
#include <set>
#include <functional>

using namespace std;
using namespace string_view_literals;

struct MyString {
    std::string inner;
};

struct MyLess {
    using is_transparent = void;
    
    template <class T>
    bool operator()(MyString const& x, T const& y) const {
        return x.inner < y;
    }

    template <class T>
    bool operator()(T const& x, MyString const& y) const {
        return x < y.inner;
    }

    bool operator()(MyString const& x, MyString const& y) const {
        return x.inner < y.inner;
    }
};

int main() {
    // (1)
    set<string> s1;
    s1.find("foo");      // implicitly creates `string`
    // s1.find("foo"sv); // compilation error

    // (1)
    set<string, less<>> s2;
    s2.find("foo");   // doesn't create `string`
    s2.find("foo"sv); // doesn't create `string`, and compiles OK
    
    // (1) (for custom types)
    // `MyString` doesn't have conversion constructors, therefore any attempt to create `MyString` in the function calls below
    // would lead to compilation errors.
    set<MyString, MyLess> s3;
    s3.find("foo");   // doesn't create `MyString`
    s3.find("foo"sv); // doesn't create `MyString`
    s3.find("foo"s);  // doesn't create `MyString`
}

The find functin method’s prototype is foo(std::string const&), which requires a string argument. Because C-strings are implicitly convertible to std::string, this function method is successfully selected in the call .find("foo") with me implicitly paying for a conversion hidden at the call point. Even though Small String Optimization alleviates this problem, sometimes the problem is unaffordable.

A transparent function object can be used to enable a templated find(T) memeber function, which solves the problem. There are void specializations of many function objects in the standard library, for example std::less<>, that essentially are transparent function objects. They provide the functionality on heterogeneous objects. It is possible to create custom transparent objects by providing using is_transparent = /*unspecified*/; type in the function object’s class scope or base class scopes.

In Rust this case works just as I expect it to work - without any need to use a custom comparator to avoid a hidden allocation.

use std::collections::HashMap;

fn main() {
    let foo: HashMap<String, u32> = HashMap::new();
    let key1 = "foo";
    let key2: String = "foo".into();

    // no implicit stirng slice to Strings, because `get` is a generic
    foo.get(key1);

    // the constraints on the generic parameter
    // require a pointer or a slice (which is a fat pointer)
    foo.get(&key2);
}
Visitor Counter