Weird errors when upgrading code from C++17 to C++20 using MSVC 2022

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.

  1. Missing typename keyword: In delete_all, T::const_iterator is a dependent name. In C++17, MSVC was lenient and allowed omitting typename for certain constructs, assuming it was a type. In C++20, the standard mandates that typename must be used to disambiguate dependent type names.
  2. Dependent Base Classes: In get_from_TRRefList_by_index, TRRefList<T>::Iterator is a dependent type name. Without the typename prefix, 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.
  3. 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 TRRefList or 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 typename keywords.
  • Refactoring Risk: Forcing the insertion of typename creates a dependency on the type being a valid type alias. If T::const_iterator is 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:

  1. Identify Dependent Names: Look for patterns like T::something where T is a template parameter.
  2. Apply typename: Add typename immediately before the dependent name when it represents a type (e.g., typename T::const_iterator).
  3. Use decltype and std::iterator_traits (Optional but robust): For maximum portability, use std::iterator_traits<typename T::const_iterator>::value_type to deduce types, though typename is sufficient for the syntax error.
  4. Update Compiler Flags: Ensure the project is actually building with /std:c++20 or /std:c++latest to verify the fix.
  5. 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:

  1. Error Code Obscurity: MSVC error codes like C3878 and C2760 are cryptic and not immediately obvious as “missing typename” errors.
  2. 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_iterator is.
  3. 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.
  4. Reliance on IDE Suggestions: Modern IDEs might auto-correct some syntax but often fail to suggest the specific typename keyword required for dependent names.