September 29th, 2025
like1 reaction

Fixing Overload Resolution For Parameter Arrays in C++/CLI

Tanveer Gani
Principal Software Design Engineer

Introduction

In the below discussion, .NET Framework refers to the older version of the Microsoft Common Language Runtime, also known as “Desktop Framework” (versions 4.0 through 4.8.x) while .NET  refers to versions .NET Core 3.1, .NET 5 and later.

The overload resolution behavior specified by the C++/CLI ECMA-372 Standard for overloads involving parameter arrays results in surprising behavior in some instances. This problem has manifested with recent versions of .NET, which added parameter array overloads to some commonly used classes, resulting in overload resolution which is surprising, even counterintuitive, and results in broken customer code when it is ported from .NET Framework to .NET. This article explains the problem and the solution adopted by the MSVC compiler to solve it.

Recap of Parameter Array Overload Resolution Rules

ECMA-372 section 14.6 details the rules governing overload resolution where parameter arrays are involved. The method to resolve overloads is that for a given parameter-array overload such as

    void f(T1 arg1, T2 arg2, <...>, Tm argm, ...array<T>^ arr);

and a corresponding call

    f(var1, var2, <...>, varm, val1, val2, <...>, valn);

a new function overload is synthesized, where the parameter array arr is replaced with parameters corresponding to the types of the formal arguments val1, val2 ... valn. Then, overload resolution is performed as usual with the additional requirement that such synthesized overloads have lower cost than a C-style variadic function (i.e., those using ... as the last argument) but higher cost than non-synthesized overloads. This requirement is in fact the cause of surprising behavior described below.

Customer Reports Previously Working Code is Broken

In .NET Framework, the String class has these methods (type names are from namespace System):

      Split(...array<Char>^);               // 1 (parameter array overload)   
      // other Split overloads not of interest

Given a function call such as:

      str->Split(L'a', L'b');               // 2

the Split overload (2) with the parameter array argument is the only one that matches. When this same code is compiled against .NET, a customer found that a different overload is instead chosen. The reason is subtle, surprising and a combination of questionable overload resolution rules in ECMA-372 and changes to the String class in .NET.

The difference between .NET Framework’s version of String and .NET’s version is that .NET added an additional overload, leading to this set of overloads:

      Split(Char, Int32, StringSplitOptions = StringSplitOptions::None); // 3
      Split(...array<Char>^);                                            // 4          

Now faced with the same function call (2) above, we see that both overloads are viable because C++ has an implicit Char (aka wchar_t) to Int32 (aka int) conversion, and the third parameter of (3) has a default argument. Due to the rule in ECMA-372/14.6, the parameter array overload (4) is deemed to have a higher cost and overload (3) is chosen, even though the arguments match the parameters exactly for overload (4). This is clearly not what the user wanted.

Microsoft C++ Solution

After discussion in the team, we noticed that there is a telling comment in ECMA-372/14.6 regarding preferring non-synthesized to synthesized overloads:

“[Note: This is similar to the tiebreaker rules for template functions and non-template functions in the C++ Standard (§13.3.3). end note]”

While the tie-breaking rule in ISO C++ does indeed exist, it comes into play only after overload resolution is done and the choice is between two overloads, one a generated function and one a non-template function. It looked like the ECMA-372 rule for choosing non-synthesized over synthesized was poorly formulated as it assigned the latter a higher cost regardless of the costs of the conversion sequences of the arguments.

In ISO C++, an analogous example would be:

    template<typename T>
    void Split(T, T);               // 5
    void Split(wchar_t, int);       // 6

The function call Split(L’a’, L’b’) will resolve to overload (5) since the arguments match exactly while one of the arguments of (6) requires a standard integral conversion. Hence, there is no need to use the tie-breaking rule at all. Were both formal arguments of (6) of type wchar_t, the tie-breaking rule would indeed come into play and choose (6) instead of giving an ambiguity error.

It became clear that ECMA-372/14.6 should have said if there is an ambiguity between two remaining overloads, one a synthesized parameter array overload, and one a non-synthesized overload, overload resolution would choose the non-synthesized one. We decided to implement this updated interpretation but there was a problem: what to do about the code currently compiling and working correctly with the older rules?

The risk of breaking older code was too great if we went unconditionally with the new rules, so we added two mechanisms to control this behavior at the compilation unit level, and whenever needed, at the function-call level.

First, we introduced new driver switches:

/clr:ECMAParamArray Turn on ECMA-372 conformant parameter array behavior
/clr:ECMAParamArray- Use updated semantics for parameter arrays

In addition, the below existing driver switches were changed to imply these corresponding modes:

/clr implies /clr:ECMAParamArray
/clr:netcore implies /clr:ECMAParamArray-

In plain language, for compilations targeting .NET Framework, we maintain the old behavior, while for compilations targeting .NET, the new behavior is the default.

To allow fine-grained control over the parameter array overload resolution mode at the function-call level, we provide a new pragma, ecma_paramarray, which can be used thus:

    #pragma ecma_paramarray(push, on)
        // Normally calls Split(Char, int32, StringSplitOptions = None) and
        // not Split(...array<Char>^) under /clr:netcore but this is
        // reverted back to the old behavior under the pragma.
        auto r = s->Split(L'a', L'b'); 
    #pragma ecma_paramarray(pop)

This pragma overrides any ambient mode for handling parameter array overload resolution, whether it comes from a prior pragma use or from the translation unit’s ambient mode.

To warn about changed behavior in overload resolution, a new warning C5306 has been implemented. Consider compiling the below under /clr:netcore:

int main()
{
    System::String^ s = "Now is the time+for all good men|to come to the aid of the party";
    auto strings = s->Split(L'|', L'+');  // Split(params char[]), not Split(char, int, System.StringSplitOptions = 0)
}

This produces the diagnostic:

    auto strings = s->Split(L'|', L'+');  // Split(params char[]), not Split(char, int, System.StringSplitOptions = 0)
                      ^
w.cpp(4,23): warning C5306: parameter array behavior change: overload 'System::String::Split' resolved to 'array<System::String ^> ^System::String::Split(...array<wchar_t> ^)'; previously, it would have resolved to 'array<System::String ^> ^System::String::Split(wchar_t,int,System::StringSplitOptions)'. Use /clr:ECMAParamArray to revert to old behavior

In addition, missing the L prefix for arguments can also produce surprising results in overload resolution with the new parameter array overload rules. A new warning C5307 was implemented to warn about this:

int main()
{
    System::String^ s = "abc def\tghi";
    // Resolves to Split(System.Char, System.Int32, System.StringSplitOptions = 0) but should warn with C5307
    auto arrs = s->Split(' ', '\t');  // Should be Split(L' ', L'\t').
}

produces

    auto arrs = s->Split(' ', '\t');  // Should be Split(L' ', L'\t').
                   ^
w.cpp(4,20): warning C5307: 'array<System::String ^> ^System::String::Split(wchar_t,int,System::StringSplitOptions)':
argument (2) converted from 'char' to 'int'. Missing 'L' encoding-prefix for character literal?

Conclusion

We would be happy to hear back from you if you have encountered problems in this area and how you solved them, and if the new way of handling parameter array overloads has helped you or hindered you.

Category
C++
Topics
C++/CLI

Author

Tanveer Gani
Principal Software Design Engineer

Tanveer Gani is a Principal Engineer with Microsoft and has been working on C++ compilers and IDEs for over two decades.

0 comments