Top 10 Most Useful C++17 Features for Real‑World Code
After a year of using C++17 in production, the author lists the ten most practical language features—grouped into syntax sugar, performance boosts, and type‑system enhancements—explaining their benefits, pitfalls, and providing concrete code examples for each.
Syntax sugar
Structured bindings allow unpacking arrays, tuples or structs directly into variables, which simplifies map iteration. Example:
// pre‑C++17
for (const auto &kv : map) {
const auto &key = kv.first;
const auto &value = kv.second;
// ...
}
// C++17
for (const auto &[key, value] : map) {
// ...
}Note: GCC permits capturing the bindings in a lambda, while Clang requires an explicit reference capture; the restriction is removed in C++20.
Class template argument deduction for std::pair/std::tuple removes the need for explicit type specifications or make_pair / make_tuple. Example:
// pre‑C++17
std::pair<int, std::string> p1{3.14, "pi"s};
auto p2 = std::make_pair(3.14, "pi"s);
// C++17
std::pair p3{3.14, "pi"s};if constexpr provides compile‑time conditional branching, replacing cumbersome SFINAE tricks. Example:
// pre‑C++17 (multiple overloads needed)
std::string convert(const char* input) { return input; }
std::string convert(const std::string& input) { return input; }
// C++17
template<typename T>
std::string convert(T input) {
if constexpr (std::is_same_v<T, const char*> || std::is_same_v<T, std::string>)
return input;
else
return std::to_string(input);
}if statement with initializer lets a variable be declared and used only inside the condition, improving readability and safety for lock handling, iterator checks, etc. Example:
int demo() {
if (auto it = m.find(10); it != m.end())
return it->second.size();
if (char buf[10]; std::fgets(buf, 10, stdin))
m[0] += buf;
if (std::lock_guard lock(mx); shared_flag) {
unsafe_ping();
shared_flag = false;
}
// …
}Performance improvements
std::shared_mutex is a native read‑write lock that enables multiple concurrent readers. A thread‑safe counter example:
class ThreadSafeCounter {
public:
ThreadSafeCounter() = default;
unsigned int get() const { std::shared_lock lock(mutex_); return value_; }
unsigned int increment() { std::unique_lock lock(mutex_); return ++value_; }
void reset() { std::unique_lock lock(mutex_); value_ = 0; }
private:
mutable std::shared_mutex mutex_;
unsigned int value_ = 0;
};std::string_view offers a non‑owning view of a string, avoiding copies and allocations. It is especially faster for substring operations; a simple benchmark shows a ~15× speedup over std::string::substr. Example:
// naive copy
std::string remove_prefix(const std::string &s) { return s.substr(3); }
// string_view version
std::string_view remove_prefix(std::string_view sv) { sv.remove_prefix(3); return sv; }try_emplace (and insert_or_assign) for associative containers inserts a key/value pair only when the key is absent, constructing the value in place without a temporary. Example:
std::map<std::string, std::string> m;
// pre‑C++17 emplace requires piecewise_construct
m.emplace(std::piecewise_construct,
std::forward_as_tuple("c"),
std::forward_as_tuple(10, 'c'));
// C++17 try_emplace is simpler
m.try_emplace("c", 10, 'c');
// insert_or_assign also handles upsert semantics
m.insert_or_assign("c", "new_value");Type system enhancements
std::any stores a copyable object of any type, preserving type information for safe casts. Compared with void*, it provides runtime type safety and automatic lifetime management, making it suitable for generic data pipelines where the exact type is unknown.
std::optional<T> models a value that may be absent, similar to Haskell’s Maybe or Rust’s Option. It replaces the old pattern of returning nullptr or a sentinel value, making failure handling explicit. Example:
// pre‑C++17
ReturnType* func(const std::string &in) {
if (in.empty()) return nullptr;
return new ReturnType;
}
// C++17
std::optional<ReturnType> func(const std::string &in) {
if (in.empty()) return std::nullopt;
return ReturnType{};
}std::variant<T, U, …> is a type‑safe union that can hold one of several alternatives, enabling sum‑type semantics. It stores the active type, allowing safe visitation via std::visit. Example of a result type:
std::variant<ReturnType, Err> func(const std::string &in) {
if (in.empty()) return Err{"input is empty"};
ReturnType ret;
// …
return ret;
}Note: C++17 lacks built‑in pattern matching; manual std::visit is required.
These three categories—syntax sugar, performance‑oriented utilities, and a richer type system—make C++17 code more concise, faster, and safer. Additional conveniences such as lambda capture of *this, std::clamp, and the [[nodiscard]] attribute further improve developer productivity.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
