What We've Been Missing in C++

C++ Development

A C++ professional delves into the language's evolution, discussing long-absent features, how developers coped, and recent C++20/23 additions, alongside what the language still lacks from a practical perspective.

Oct 29 2025

What features are missing from C++ and how do developers bridge these gaps? This article delves into concepts and examples illustrating how the language could evolve further. Authored by Nikolai Shalakin, this piece is published and translated with the copyright holder's permission.

The Long Quest for Missing Features

With over a decade of professional C++ development experience since 2013, I've witnessed the language's transformation with triennial standard updates. C++11, for instance, introduced revolutionary features, yet many developers remained tied to older standards like C++03, observing new advancements with a degree of envy.

Throughout these years, a consistent pattern emerged across diverse projects: the reliance on "helper" files and custom containers that frequently re-implemented functionalities addressing gaps in the Standard Template Library (STL). These aren't highly specialized algorithms but rather fundamental elements deemed essential for robust C++ software development. It's common to observe different companies arriving at identical custom solutions, indicating a natural demand for features not yet supplied by the STL.

This article compiles some of the most notable examples from my development experience. While researching, I found that some previously missing features have since been fully or partially integrated into newer language standards. Therefore, this discussion serves as both a reflection on and a critique of long-awaited features that have finally arrived, as well as an exploration of what remains absent from the C++ standard. This article aims not to make grand claims, but to foster a conversation about practical, everyday C++ development.

Disclaimer: For simplicity within this article, the terms C++, STL, language, and language standard may be used interchangeably.

What Was Missing for a Long Time

std::string::starts_with, std::string::ends_with

This has been a persistent omission for many C++ developers, features we eagerly anticipated for a considerable time. Many C++ developers have likely encountered similar implementations in their codebases:

inline bool starts_with(const std::string &s1, const std::string &s2)
{
  return s2.size() <= s1.size() && s1.compare(0, s2.size(), s2) == 0;
}

These methods were finally introduced in C++20, a standard still not universally adopted. Fortunate developers can now, however, readily find string prefixes and postfixes:

std::string s("c++20");

bool res1 = s.starts_with("c++"); // true
bool res2 = s.starts_with("c#");  // false
bool res3 = s.ends_with("20");    // true
bool res4 = s.ends_with("27");    // false

std::optional

Some might suggest that std::optional has been a staple for years. Indeed, it has been with us since C++17, and its utility is widely appreciated. However, this is a personal reflection. In my early career, I worked on a project strictly adhering to C++03, necessitating the use of a custom optional class developed by a colleague.

Reviewing the implementation of this custom optional was an insightful experience for a junior developer. Though straightforward, it evoked a similar sense of excitement to poring over STL source code.

I'm pleased that today, I can confidently and without hesitation write code like this in most projects:

std::optional<Result> getResult();

const auto res = getResult();
if (res) {
  std::cout << *res << std::endl;
} else {
  std::cout << "No result!" << std::endl;
}

std::expected

For those familiar with Rust, the Option<T> class has a close counterpart: Result<T, E>. These are intimately related, offering methods to convert between them.

While Option<T> is analogous to C++'s std::optional<T>, Result<T, E> warrants further explanation. It's similar to optional<T>, but instead of simply indicating absence, it signifies an error of type E when no valid result is present. Thus, a Result<T, E> object can exist in one of two states:

  • The Ok state, holding a valid value of type T.
  • The Error state, holding an error of type E.

One can query the object's state and attempt to retrieve either the valid value or the associated error.

Such a class might seem unconventional to C++ developers, but it's crucial in Rust, where exceptions are absent, and errors are primarily handled by returning error codes. In the vast majority of cases, this is achieved by returning a Result<T, E> object.

Coincidentally, during my C++ career, I've primarily worked on projects where exceptions were prohibited for various reasons. In such contexts, C++ error handling conceptually aligns with Rust's approach.

Observing Result<T, E> in Rust left a lasting impression, fueling my desire for a similar feature in C++. Consequently, I developed a C++ analog, somewhat awkwardly named Maybe<T, E> (which could confuse Haskell programmers, where Maybe corresponds to optional).

Recently, I discovered that the C++ standardization committee approved std::expected<T, E> in the C++23 standard. Microsoft Visual C++ (MSVC) has even implemented it in VS 2022 17.3, accessible with the /std:c++latest compiler option. The chosen name, std::expected, is fitting and arguably superior to Result or Maybe.

Let's illustrate its use with code that parses a human-readable chess address (e.g., "a3") into zero-indexed coordinates (e.g., [2; 0]) for a chess engine:

struct ChessPosition
{
  int row; // stored as [0; 7], represents [1; 8]
  int col; // stored as [0; 7], represents [a; h]
};

enum class ParseError
{
  InvalidAddressLength,
  InvalidRow,
  InvalidColumn
};

auto parseChessPosition(std::string_view address) -> 
                    std::expected<ChessPosition, ParseError>
{
  if (address.size() != 2) {
    return std::unexpected(ParseError::InvalidAddressLength);
  }

  int col = address[0] - 'a';
  int row = address[1] - '1';

  if (col < 0 || col > 7) {
    return std::unexpected(ParseError::InvalidColumn);
  }

  if (row < 0 || row > 7) {
    return std::unexpected(ParseError::InvalidRow);
  }

  return ChessPosition{ row, col };
}

...

auto res1 = parseChessPosition("e2");  // [1; 4]
auto res2 = parseChessPosition("e4");  // [3; 4]
auto res3 = parseChessPosition("g9");  // InvalidRow
auto res4 = parseChessPosition("x3");  // InvalidColumn
auto res5 = parseChessPosition("e25"); // InvalidAddressLength

std::bit_cast

I've often encountered scenarios requiring unusual bit manipulation, such as obtaining the bit representation of a floating-point number. In my early career, I sometimes encountered undefined behavior (UB) by using simple, yet risky, type conversions. Here are some unsafe bit representation conversion methods:

First, reinterpret_cast. It's tempting and straightforward to write code like this:

uint32_t i = *reinterpret_cast<uint32_t*>(&f);

However, this leads to undefined behavior.

Returning to C-style casts, which are essentially reinterpret_cast but syntactically simpler:

uint32_t i = *(uint32_t*)&f;

One might recall its use by Quake III developers, yet it remains UB.

The union trick:

union {
  float f;
  uint32_t i;
} value32;

The declaration itself isn't UB, but reading from a union member different from the one last written to is also undefined behavior.

I've observed all these approaches, with various motivations:

  • Determining the sign of a float by examining its most significant bit.
  • Converting a pointer to an integer and back – common in embedded systems, including unusual cases like converting an address into an ID.
  • Mathematical manipulations involving the exponent or mantissa of a float.

Some might wonder about the utility of a mantissa. I addressed this in an old GitHub project where I created a simple IEEE 754 converter for educational purposes. It allows experimenting with the bit representation of 32-bit floating-point numbers and was also an exercise in recreating the Windows 7 calculator design.

Ultimately, such bit-level operations are occasionally necessary. The crucial question is how to perform them safely. When I consulted Stack Overflow, I received a clear, albeit stark, answer: memcpy. I also adopted a small snippet from there to facilitate its use:

template <class OUT, class IN>
inline OUT bit_cast(IN const& in)
{
  static_assert(sizeof(OUT) == sizeof(IN), 
                "source and dest must be same size");
  static_assert(std::is_trivially_copyable<OUT>::value,
                "destination type must be trivially copyable.");
  static_assert(std::is_trivially_copyable<IN>::value,
                "source type must be trivially copyable");
  
  OUT out;
  memcpy(&out, &in, sizeof(out));
  return out;
}

C++20 introduced std::bit_cast, which performs the same task but is constexpr. This was made possible by compiler capabilities required by the standard.

Now, we can appreciate its elegance and verify its correctness according to language specifications, for example, in the famous inverse square root function:

float q_rsqrt(float number)
{
  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y = number;
  i = std::bit_cast<long>(y);          // evil floating point bit level hacking
  i = 0x5f3759df - (i >> 1);           // what the fuck?
  y = std::bit_cast<float>(i);
  y = y * (threehalfs - (x2 * y * y));    // 1st iteration
  //y = y * (threehalfs - (x2 * y * y));  // 2nd iteration, this can be removed

  return y;
}

Acknowledging its history, id Software.

What Is Still Missing and May Never Come to Be

Floating-Point Arithmetic

As is widely known, directly comparing two floating-point numbers for equality is problematic. Even if they appear identical, values like 1.0 and 0.999999999 are not strictly equal. The language currently lacks standard methods to adequately address this; developers must manually compare the absolute difference between numbers against a chosen epsilon value.

Another useful, but missing, feature is the ability to round a number to a specific number of decimal places. While floor, ceil, and round are available, they all round to the nearest integer, not to a specified precision. Consequently, developers frequently resort to Stack Overflow for custom solutions.

This often leads to the proliferation of helper functions within a codebase, such as these:

template<class T>
bool almostEqual(T x, T y)
{
  return std::abs(x - y) < std::numeric_limits<T>::epsilon();
}

template<class T>
bool nearToZero(T x)
{
  return std::abs(x) < std::numeric_limits<T>::epsilon();
}

template<class T>
T roundTo(T x, uint8_t digitsAfterPoint)
{
  const uint32_t delim = std::pow(10, digitsAfterPoint);
  return std::round(x * delim) / delim;
}

While not a critical flaw, this lack of standardized support is noteworthy.

EnumArray

Consider a simple enumeration:

enum class Unit
{
  Grams,
  Meters,
  Liters,
  Items
};

It's a common requirement to associate configuration or information with each enumerator, often necessitating a dictionary-like structure with an enum key. A straightforward solution using STL might look like this:

std::unordered_map<Unit, const char*> unitNames {
  { Unit::Grams, "g" },
  { Unit::Meters, "m" },
  { Unit::Liters, "l" },
  { Unit::Items, "pcs" },
};

However, this approach has considerations:

  • std::unordered_map is not the most generic or memory-efficient container.
  • Such configuration dictionaries are frequent, but typically small, rarely exceeding a few dozen items. Using a hash table (std::unordered_map) or a tree (std::map) can feel excessive.
  • An enumeration is fundamentally a numerical value, making it tempting to use it as a numerical index.

This line of thought quickly leads to the idea of a container that presents a dictionary interface but is backed by std::array. The array's indices would correspond to enum elements, and its data would store the map's values.

The challenge lies in determining the array's size. A simple, time-tested approach is to add a Count service element to the end of the enum. This method is widely observed in codebases and is generally acceptable:

enum class Unit
{
  Grams,
  Meters,
  Liters,
  Items,
  
  Count
};

The EnumArray proxy container implementation is then quite simple:

template<typename Enum, typename T>
class EnumArray
{
public:
  EnumArray(std::initializer_list<std::pair<Enum, T>>&& values);

  T& operator[](Enum key);
  const T& operator[](Enum key) const;

private:
  static constexpr size_t N = std::to_underlying(Enum::Count);
  std::array<T, N> data;
};

The constructor with std::initializer_list allows configuration similar to std::unordered_map:

EnumArray<Unit, const char*> unitNames {
  { Unit::Grams, "g" },
  { Unit::Meters, "m" },
  { Unit::Liters, "l" },
  { Unit::Items, "pcs" },
};
std::cout << unitNames[Unit::Items] << std::endl; // outputs "pcs"

This approach offers several advantages:

  • It combines the convenience of a dictionary interface with the efficiency and simplicity of std::array.
  • Unlike std::unordered_map and std::map, it's cache-friendly due to sequential memory storage.
  • The array size is known at compile-time, allowing most methods to be constexpr with further refinement.

However, there are limitations:

  • The enumeration must include a Count element.
  • The enumeration cannot have custom type values (e.g., enum class Type { A = 4, B = 12, C = 518, D }). It requires default sequential ordering from zero.
  • Memory is allocated for all enum elements in the array. If not all values are explicitly filled in EnumArray, the remaining elements will be default-constructed.
  • Consequently, the type T must be default-constructible.

Despite these restrictions, I frequently use this container without issue.

Early Return

Consider a typical function with multiple boundary checks:

std::string applySpell(Spell* spell)
{
  if (!spell)
  {
    return "No spell";
  }

  if (!spell->isValid())
  {
    return "Invalid spell";
  }

  if (this->isImmuneToSpell(spell))
  {
    return "Immune to spell";
  }

  if (this->appliedSpells.constains(spell))
  {
    return "Spell already applied";
  }

  appliedSpells.append(spell);
  applyEffects(spell->getEffects());
  return "Spell applied";
}

This is standard practice. The actual logic is confined to the few lines at the bottom, preceded by extensive validation. This can be somewhat cumbersome, especially when following coding styles like Allman, where each curly brace defines its own block.

An ideal solution would be a more streamlined approach, reducing boilerplate. C++'s assert offers a similar mechanism: it checks a condition and, if necessary, takes an action. However, assert doesn't need to return a value. Still, we could create something analogous:

#define early_return(cond, ret)      \
  do {                             \
    if (static_cast<bool>(cond)) \
    {                            \
      return ret;              \
    }                            \
  } while (0)

#define early_return_void(cond)      \
  do {                             \
    if (static_cast<bool>(cond)) \
    {                            \
      return;                  \
    }                            \
  } while (0)

The use of macros, however, often draws criticism, notably from Bjarne Stroustrup. If he were to send a personal message requesting an apology, I would understand and oblige, as I also generally dislike C++ macros.

Indeed, the code includes two macros. We can consolidate them into one using a variadic macro:

#define early_return(cond, ...)      \
  do {                             \
    if (static_cast<bool>(cond)) \
    {                            \
      return __VA_ARGS__;      \
    }                            \
  } while (0)

Only one macro remains, but it's still a macro. A non-macro conversion isn't feasible here; once encapsulated within a function, we lose the ability to directly influence the control flow of the calling function. It's an unfortunate reality. However, observe how our example can be rewritten:

std::string applySpell(Spell* spell)
{
  early_return(!spell, "No spell");
  early_return(!spell->isValid(), "Invalid spell");
  early_return(this->isImmuneToSpell(spell), "Immune to spell");
  early_return(this->appliedSpells.constains(spell), "Spell already applied");

  appliedSpells.append(spell);
  applyEffects(spell->getEffects());
  return "Spell applied";
}

This also works for void functions:

void applySpell(Spell* spell)
{
  early_return(!spell);
  early_return(!spell->isValid());
  early_return(this->isImmuneToSpell(spell));
  early_return(this->appliedSpells.constains(spell));

  appliedSpells.append(spell);
  applyEffects(spell->getEffects());
}

The code is shortened and, I believe, improved. If this were a standard feature, it could be a full-fledged language construct rather than a macro. It's worth noting, somewhat ironically, that assert in C++ is also a macro.

If one strictly adheres to assert's behavior – asserting expected conditions and triggering action otherwise – we can easily adapt the logic and rename the macro:

#define ensure_or_return(cond, ...)   \
  do {                              \
    if (!static_cast<bool>(cond)) \
    {                             \
      return __VA_ARGS__;       \
    }                             \
  } while (0)

void applySpell(Spell* spell)
{
  ensure_or_return(spell);
  ensure_or_return(spell->isValid());
  ensure_or_return(!this->isImmuneToSpell(spell));
  ensure_or_return(!this->appliedSpells.constains(spell));

  appliedSpells.append(spell);
  applyEffects(spell->getEffects());
}

While the name could be refined, the underlying concept is clear. I would welcome any of these constructs in C++.

Unordered Erase

std::vector is arguably the most frequently used collection in C++. While versatile, it's generally inefficient for inserting or deleting items from arbitrary positions, as these operations typically take O(n) time. The necessity of shuffling roughly half the vector's contents for a slight shift is often frustrating.

An idiomatic trick transforms O(n) deletion into O(1), though it sacrifices the original order of elements. If order preservation is not critical, this simple technique is highly effective:

std::vector<int> v {
  17, -2, 1084, 1, 17, 40, -11
};

// We delete the element '1' from the vector
std::swap(v[3], v.back()); 
v.pop_back();

// We get [17, -2, 1084, -11, 17, 40]

The method involves swapping the element to be deleted with the last element of the vector, then simply removing the last element. Both operations are extremely cheap, making it a simple and elegant solution.

It's unclear why the std::vector interface doesn't offer such a straightforward alternative to the standard erase method; Rust, for example, includes this functionality. Consequently, we often create our own helper function:

template<typename T>
void unorderedErase(std::vector<T>& v, int index)
{
  std::swap(v[index], v.back());
  v.pop_back();
}

Conclusion

As I compiled this article, I found myself revising and discarding significant portions, as recent C++20 and C++23 standards have already addressed many items on my initial 'wish list.' Indeed, user requests are diverse and numerous, making it impossible to integrate every suggestion into the standard library or language itself.

I aimed to highlight only the points I consider least subjective and most deserving of inclusion in the language standard. In my professional experience, these features are almost daily necessities. You may, of course, hold a different perspective on my list, and I welcome discussions in the comments regarding your own pain points and desired features, as this provides valuable insight into the future direction of C++.