Summary
Upgrading a C++17 codebase to C++20 using MSVC 2022 triggered a series of obscure compiler errors (C3878, C2760, C2065, C7510) in template-heavy code. The root cause was the two-phase name lookup changes introduced in C++20, specifically the requirement to use the typename keyword for dependent type names. The template code relied on pre-C++20 behaviors where dependent names were implicitly treated as types, causing the compiler to fail when resolving the const_iterator inside the delete_all template and the Iterator in get_from_TRRefList_by_index.
Root Cause
The fundamental issue is the strict name lookup rules enforced in C++20, which affect how compilers resolve dependent names (names that depend on a template parameter) during the first phase of template compilation.
- Missing
typenamekeyword: Indelete_all,T::const_iteratoris a dependent name. In C++17, MSVC was lenient and allowed omittingtypenamefor certain constructs, assuming it was a type. In C++20, the standard mandates thattypenamemust be used to disambiguate dependent type names. - Dependent Base Classes: In
get_from_TRRefList_by_index,TRRefList<T>::Iteratoris a dependent type name. Without thetypenameprefix, the compiler attempts to resolve it as a non-type expression or fails to recognize it as a valid type during the first phase of template parsing. - Compiler Version: MSVC 17.14.24 implements the C++20 standard strictly. The errors C3878 and C2760 are specific MSVC diagnostics related to syntax violations in dependent contexts, often triggered when the compiler expects a type keyword but finds an identifier instead.
Why This Happens in Real Systems
This is a common friction point during language standard upgrades because:
- Legacy Codebases: Many C++ codebases predate strict C++20 requirements. They accumulated implicit dependencies on compiler-specific leniency (MSVC’s pre-C++20 behavior).
- Header-Only Libraries: If the code relies on header-only templates, the compiler must validate syntax at the point of definition (Phase 1). C++20 forces stricter validation, causing failures in code that “worked” previously simply because the compiler deferred the check or made assumptions.
- Generic Programming: Generic code interacting with third-party libraries (like the hypothetical
TRRefListor standard containers) often exposes these issues when the container types change or when the compiler’s parsing logic evolves.
Real-World Impact
- Compilation Failure: The code will not compile under C++20 without modifications, blocking the upgrade.
- Developer Productivity: Engineers waste time debugging obscure error codes (C3878, C2760) rather than immediately identifying the missing
typenamekeywords. - Refactoring Risk: Forcing the insertion of
typenamecreates a dependency on the type being a valid type alias. IfT::const_iteratoris actually a static member or a value, the fix will introduce a different error, requiring deeper analysis of the template constraints.
Example or Code
The provided code snippets fail because they lack the typename keyword required to tell the compiler that const_iterator and Iterator are types dependent on the template parameters.
Incorrect Code (Fails in C++20):
template
void delete_all(T& t) {
// Error C2065/C2760: 'const_iterator' is a dependent name.
// Without 'typename', the compiler doesn't know it's a type.
for (T::const_iterator it = t.begin(); it != t.end(); ++it) {
delete *it;
}
t.clear();
}
template
inline T* get_from_TRRefList_by_index(const TRRefList& liste, unsigned int index) {
assert(!(index >= liste.Count() || index < 0));
// Error C3878/C7510: 'Iterator' is a dependent name.
TRRefList::Iterator x(liste);
for (unsigned int i = 0; i < liste.Count(); ++i, ++x) {
if (i == index) return x.current();
}
return nullptr;
}
Corrected Code (Valid in C++20):
template
void delete_all(T& t) {
// 'typename' explicitly declares that 'const_iterator' is a type.
for (typename T::const_iterator it = t.begin(); it != t.end(); ++it) {
delete *it;
}
t.clear();
}
template
inline T* get_from_TRRefList_by_index(const TRRefList& liste, unsigned int index) {
assert(!(index >= liste.Count() || index < 0));
// 'typename' is required here as well.
typename TRRefList::Iterator x(liste);
for (unsigned int i = 0; i < liste.Count(); ++i, ++x) {
if (i == index) return x.current();
}
return nullptr;
}
How Senior Engineers Fix It
Senior engineers approach this systematically rather than blindly adding keywords:
- Identify Dependent Names: Look for patterns like
T::somethingwhereTis a template parameter. - Apply
typename: Addtypenameimmediately before the dependent name when it represents a type (e.g.,typename T::const_iterator). - Use
decltypeandstd::iterator_traits(Optional but robust): For maximum portability, usestd::iterator_traits<typename T::const_iterator>::value_typeto deduce types, thoughtypenameis sufficient for the syntax error. - Update Compiler Flags: Ensure the project is actually building with
/std:c++20or/std:c++latestto verify the fix. - Static Assertions: Add
static_assert(std::is_class_v<T>, "T must be a class type");to provide clearer error messages if the template is instantiated incorrectly.
Why Juniors Miss It
Junior developers often struggle with these errors because:
- Error Code Obscurity: MSVC error codes like C3878 and C2760 are cryptic and not immediately obvious as “missing typename” errors.
- Lack of Two-Phase Knowledge: They may not understand the distinction between Phase 1 (parsing) and Phase 2 (instantiation) in template compilation, assuming the compiler should “know” what
T::const_iteratoris. - Context Blindness: When upgrading a massive codebase, the sheer volume of errors can be overwhelming. A junior dev might try to fix the syntax logic (loops, conditions) rather than the type system requirements.
- Reliance on IDE Suggestions: Modern IDEs might auto-correct some syntax but often fail to suggest the specific
typenamekeyword required for dependent names.