{"id":35772,"date":"2025-09-29T14:00:08","date_gmt":"2025-09-29T14:00:08","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cppblog\/?p=35772"},"modified":"2025-09-24T03:57:56","modified_gmt":"2025-09-24T03:57:56","slug":"fixing-overload-resolution-for-parameter-arrays-in-c-cli","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/cppblog\/fixing-overload-resolution-for-parameter-arrays-in-c-cli\/","title":{"rendered":"Fixing Overload Resolution For Parameter Arrays in C++\/CLI"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p><span style=\"font-size: 16px;\">In the below discussion, <\/span><code style=\"font-size: 16px;\">.NET Framework<\/code><span style=\"font-size: 16px;\"> refers to the older version\u00a0<\/span>of the Microsoft Common Language Runtime, also known as &#8220;Desktop Framework&#8221; (versions 4.0 through 4.8.x) while <code>.NET<\/code>\u00a0 refers to versions <code>.NET Core 3.1<\/code>, <code>.NET 5<\/code> and later.<\/p>\n<p>The overload resolution behavior specified by the C++\/CLI\u00a0ECMA-372\nStandard for overloads involving parameter arrays results in surprising\nbehavior in some instances. This problem has manifested with recent\nversions of .NET, which added parameter array overloads to some\ncommonly used classes, resulting in overload resolution which is\nsurprising, even counterintuitive, and results in broken customer code\nwhen it is ported from .NET Framework to .NET. This article\nexplains the problem and the solution adopted by the MSVC\ncompiler to solve it.<\/p>\n<h2>Recap of Parameter Array Overload Resolution Rules<\/h2>\n<p>ECMA-372 section 14.6 details the rules governing overload resolution\nwhere parameter arrays are involved. The method to resolve overloads is\nthat for a given parameter-array overload such as<\/p>\n<pre><code class=\"language-c++\">    void f(T1 arg1, T2 arg2, &lt;...&gt;, Tm argm, ...array&lt;T&gt;^ arr);<\/code><\/pre>\n<p>and a corresponding call<\/p>\n<pre><code class=\"language-c++\">    f(var1, var2, &lt;...&gt;, varm, val1, val2, &lt;...&gt;, valn);<\/code><\/pre>\n<p>a new function overload is synthesized, where the parameter array <code>arr<\/code>\nis replaced with parameters corresponding to the types of the formal\narguments <code>val1, val2 ... valn<\/code>. Then, overload resolution is performed\nas usual with the additional requirement that such synthesized overloads\nhave lower cost than a C-style variadic function (i.e., those using\n<code>...<\/code> as the last argument) but higher cost than non-synthesized\noverloads. This requirement is in fact the cause of surprising behavior\ndescribed below.<\/p>\n<h2>Customer Reports Previously Working Code is Broken<\/h2>\n<p>In .NET Framework, the <code>String<\/code> class has these methods (type names\nare from namespace <code>System<\/code>):<\/p>\n<pre><code class=\"language-c++\">      Split(...array&lt;Char&gt;^);               \/\/ 1 (parameter array overload)   \r\n      \/\/ other Split overloads not of interest<\/code><\/pre>\n<p>Given a function call such as:<\/p>\n<pre><code class=\"language-c++\">      str-&gt;Split(L'a', L'b');               \/\/ 2<\/code><\/pre>\n<p>the <code>Split<\/code> overload (2) with the parameter array argument is the only\none that matches. When this same code is compiled against .NET, a\ncustomer found that a different overload is instead chosen. The reason\nis subtle, surprising and a combination of questionable overload\nresolution rules in ECMA-372 and changes to the <code>String<\/code> class in .NET.<\/p>\n<p>The difference between .NET Framework&#8217;s\u00a0version of <code>String<\/code> and .NET&#8217;s version is that .NET added an additional overload, leading\nto this set of overloads:<\/p>\n<pre><code class=\"language-c++\">      Split(Char, Int32, StringSplitOptions = StringSplitOptions::None); \/\/ 3\r\n      Split(...array&lt;Char&gt;^);                                            \/\/ 4          <\/code><\/pre>\n<p>Now faced with the same function call (2) above, we see that both\noverloads are viable because C++ has an implicit <code>Char<\/code> (aka <code>wchar_t<\/code>) to\n<code>Int32<\/code> (aka <code>int<\/code>) conversion, and the third parameter of (3) has a default\nargument. Due to the rule in ECMA-372\/14.6, the parameter array overload\n(4) is deemed to have a higher cost and overload (3) is chosen, <em>even\nthough the arguments match the parameters exactly for overload (4)<\/em>.\nThis is clearly not what the user wanted.<\/p>\n<h2>Microsoft C++ Solution<\/h2>\n<p>After discussion in the team, we noticed that there is a telling comment\nin ECMA-372\/14.6 regarding preferring non-synthesized to synthesized\noverloads:<\/p>\n<blockquote><p>&#8220;[<em>Note<\/em>: This is similar to the tiebreaker rules for template\nfunctions and non-template functions in the C++ Standard (\u00a713.3.3).\n<em>end note<\/em>]&#8221;<\/p><\/blockquote>\n<p>While the tie-breaking rule in ISO C++ does indeed exist, <em>it comes into\nplay only after overload resolution is done and the choice is between\ntwo overloads, one a generated function and one a non-template\nfunction<\/em>. It looked like the ECMA-372 rule for choosing non-synthesized\nover synthesized was poorly formulated as it assigned the latter a\nhigher cost regardless of the costs of the conversion sequences of the\narguments.<\/p>\n<p>In ISO C++, an analogous example would be:<\/p>\n<pre><code class=\"language-c++\">    template&lt;typename T&gt;\r\n    void Split(T, T);               \/\/ 5\r\n    void Split(wchar_t, int);       \/\/ 6<\/code><\/pre>\n<p>The function call <code>Split(L\u2019a\u2019, L\u2019b\u2019)<\/code> will resolve to overload (5) since\nthe arguments match exactly while one of the arguments of (6) requires a\nstandard integral conversion. Hence, there is no need to use the\ntie-breaking rule at all. Were both formal arguments of (6) of type <code>wchar_t<\/code>, the\ntie-breaking rule would indeed come into play and choose (6) instead of\ngiving an ambiguity error.<\/p>\n<p>It became clear that ECMA-372\/14.6 should have said if there is an\nambiguity between two remaining overloads, one a synthesized parameter\narray overload, and one a non-synthesized overload, overload resolution\nwould choose the non-synthesized one. We decided to implement this\nupdated interpretation but there was a problem: what to do about the\ncode currently compiling and working correctly with the older rules?<\/p>\n<p>The risk of breaking older code was too great if we went unconditionally\nwith the new rules, so we added two mechanisms to control this behavior\nat the compilation unit level, and whenever needed, at the function-call\nlevel.<\/p>\n<p>First, we introduced new driver switches:<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>\/clr:ECMAParamArray<\/code><\/td>\n<td>Turn on ECMA-372 conformant parameter array behavior<\/td>\n<\/tr>\n<tr>\n<td><code>\/clr:ECMAParamArray-<\/code><\/td>\n<td>Use updated semantics for parameter arrays<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In addition, the below existing driver switches were changed to imply these\ncorresponding modes:<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>\/clr<\/code><\/td>\n<td>implies <code>\/clr:ECMAParamArray<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>\/clr:netcore<\/code><\/td>\n<td>implies <code>\/clr:ECMAParamArray-<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In plain language, for compilations targeting .NET Framework, we\nmaintain the old behavior, while for compilations targeting .NET, the\nnew behavior is the default.<\/p>\n<p>To allow fine-grained control over the parameter array overload\nresolution mode at the function-call level, we provide a new pragma,\n<code>ecma_paramarray<\/code>, which can be used thus:<\/p>\n<pre><code class=\"language-c++\">    #pragma ecma_paramarray(push, on)\r\n        \/\/ Normally calls Split(Char, int32, StringSplitOptions = None) and\r\n        \/\/ not Split(...array&lt;Char&gt;^) under \/clr:netcore but this is\r\n        \/\/ reverted back to the old behavior under the pragma.\r\n        auto r = s-&gt;Split(L'a', L'b'); \r\n    #pragma ecma_paramarray(pop)<\/code><\/pre>\n<p>This pragma overrides any ambient mode for handling parameter array\noverload resolution, whether it comes from a prior pragma use or from\nthe translation unit&#8217;s ambient mode.<\/p>\n<p>To warn about changed behavior in overload resolution, a new warning\n<code>C5306<\/code> has been implemented. Consider compiling the below under <code>\/clr:netcore<\/code>:<\/p>\n<pre><code class=\"language-c++\">int main()\r\n{\r\n    System::String^ s = \"Now is the time+for all good men|to come to the aid of the party\";\r\n    auto strings = s-&gt;Split(L'|', L'+');  \/\/ Split(params char[]), not Split(char, int, System.StringSplitOptions = 0)\r\n}<\/code><\/pre>\n<p>This produces the diagnostic:<\/p>\n<pre><code class=\"language-txt\">    auto strings = s-&gt;Split(L'|', L'+');  \/\/ Split(params char[]), not Split(char, int, System.StringSplitOptions = 0)\r\n                      ^\r\nw.cpp(4,23): warning C5306: parameter array behavior change: overload 'System::String::Split' resolved to 'array&lt;System::String ^&gt; ^System::String::Split(...array&lt;wchar_t&gt; ^)'; previously, it would have resolved to 'array&lt;System::String ^&gt; ^System::String::Split(wchar_t,int,System::StringSplitOptions)'. Use \/clr:ECMAParamArray to revert to old behavior<\/code><\/pre>\n<p>In addition, missing the <code>L<\/code> prefix for arguments can also produce\nsurprising results in overload resolution with the new parameter array\noverload rules. A new warning <code>C5307<\/code> was implemented to warn about\nthis:<\/p>\n<pre><code class=\"language-c++\">int main()\r\n{\r\n    System::String^ s = \"abc def\\tghi\";\r\n    \/\/ Resolves to Split(System.Char, System.Int32, System.StringSplitOptions = 0) but should warn with C5307\r\n    auto arrs = s-&gt;Split(' ', '\\t');  \/\/ Should be Split(L' ', L'\\t').\r\n}<\/code><\/pre>\n<p>produces<\/p>\n<pre><code class=\"language-txt\">    auto arrs = s-&gt;Split(' ', '\\t');  \/\/ Should be Split(L' ', L'\\t').\r\n                   ^\r\nw.cpp(4,20): warning C5307: 'array&lt;System::String ^&gt; ^System::String::Split(wchar_t,int,System::StringSplitOptions)':\r\nargument (2) converted from 'char' to 'int'. Missing 'L' encoding-prefix for character literal?<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p>We would be happy to hear back from you if you have encountered\nproblems in this area and how you solved them, and if the new way of\nhandling parameter array overloads has helped you or hindered you.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Fix a problem in C++\/CLI parameter array overload resolution which affects newer .NET versions.<\/p>\n","protected":false},"author":67691,"featured_media":35994,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[86],"class_list":["post-35772","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cplusplus","tag-ccli"],"acf":[],"blog_post_summary":"<p>Fix a problem in C++\/CLI parameter array overload resolution which affects newer .NET versions.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts\/35772","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/users\/67691"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/comments?post=35772"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/posts\/35772\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/media\/35994"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/media?parent=35772"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/categories?post=35772"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/cppblog\/wp-json\/wp\/v2\/tags?post=35772"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}