Safer Proxy Idiom

By Michael Park

Introduction

Today I want to tackle a very specific problem around proxy objects such as std::vector<bool>::reference. The general pattern here is that the object being returned could be holding references to a temporary object and therefore needs to be consumed before the next sequence point. This is roughly the wording employed to describe the behavior of std::forward_as_tuple as well.

If you need a more credible description of this problem, you can refer to Item 6 of Scott Meyers’ Effective Modern C++ titled: “Use the explicitly typed initializer idiom when auto deduces undesired types.”

The Problem

The main issue with proxy objects such as std::vector<bool>::reference and std::forward_as_tuple is that often they are designed to only live for a single statement. Suppose I have a proxy object Join which is returned from a string join function. In this case expectation is that the user immediately consumes the proxy object. They can do so by streaming it out, or actually construct a string out of it.

class Join {
  public:

  // Implicitly convert to a `std::string`.
  operator std::string() && {
    std::ostringstream strm;
    strm << *this;
    return std::move(strm).str();
  }

  explicit Join(const std::string &delimiter,
                const std::string &lhs,
                const std::string &rhs)
      : delimiter_(delimiter), lhs_(lhs), rhs_(rhs) {}

  private:

  // References to the parts.
  const std::string &delimiter_;
  const std::string &lhs_;
  const std::string &rhs_;

  // Directly stream the arguments out to the stream.
  friend std::ostream &operator<<(std::ostream &strm, const Join &join) {
    return strm << join.lhs_ << join.delimiter_ << join.rhs_;
  }
};

Join join(const std::string &delimiter,
          const std::string &lhs,
          const std::string &rhs) {
  return Join(delimiter, lhs, rhs);
}

It can be used like so:

// Construct a string and use it.
std::string str = join(", ", "a", "b");
std::cout << str << std::endl;

// Streaming it out directly avoids the temporary string unlike the above.
std::cout << join(", ", "hello", "world") << std::endl;

This is great, unless we use it incorrectly of course. One subtle pitfall here is if we were to replace std::string with auto. For example:

// Construct a string? and use it...
auto str = join(", ", "a", "b");

// Not safe! `str` could be holding references to temporary objects!
std::cout << str << std::endl;

Here we’ve arrived at undefined behavior land. The reason is because str is a proxy object which could be holding references to temporary objects (which, in this case it is). The temporary objects that str is holding references of have gone out of scope by the time we get to using it.

How Can We Help?

Our goal is to make the interface harder to use incorrectly by disallowing statements of the following form:

auto str = join(", ", "a", "b");

Implementation

We need to disallow the public use of any of the constructors of Join, but allow join to access them since it returns Join by value. Well, friend sounds like the right candidate for the job. We make all the constructors private and then make join a friend.

class Join {
  public:

  // Implicitly convert to a `std::string`.
  operator std::string() && { /* unchanged */ }

  private:

  explicit Join(const std::string &delimiter,
                const std::string &lhs,
                const std::string &rhs)
      : delimiter_(delimiter), lhs_(lhs), rhs_(rhs) {}

  Join(const Join &) = default;
  Join(Join &&) = default;

  // References to the parts.
  /* unchanged */

  // Directly stream the arguments out to the stream.
  friend std::ostream &operator<<(std::ostream &strm, const Join &join) {
    /* unchanged */
  }

  friend Join join(const std::string &delimiter,
                   const std::string &lhs,
                   const std::string &rhs);
};

Join join(const std::string &delimiter,
          const std::string &lhs,
          const std::string &rhs) {
  return Join(delimiter, lhs, rhs);
}

Now the attempts to “save” an instance of Join will be disallowed.

// error: calling a private constructor of class 'Join'
auto str = join(", ", "a", "b");

That’s it! This approach works for cases where the expected result from a function is a value. In our case, it’s probably helpful since we’re essentially expecting a std::string as the result of join, Join works seamlessly with that expectation, with a compile-time error when you try to capture it with auto.

Limitations

As of now, I don’t know of a way to completely prevent bad behavior. But in a language where we can bind references to a temporary object, there’s not much we can do: const auto &ref = join(", ", "a", "b");

You may look at the above code and say, “but the lifetime of the temporary is extended to the lifetime of the reference!” and yes, it absolutely is. However, it does not extend the lifetime of the temporary we want to extend. In our case, we simply extend the lifetime of the temporary proxy object. Not the temporary objects that Join holds! If join were to return a std::string, this approach would work. Binding a reference to a temporary object however is bad practice in general, so this limitation doesn’t worry me too much.