﻿# Should we initialize an out parameter before a method returns?

Surely every C\# developer has used out\-parameters\. It seems that everything is extremely simple and clear with them\. But is it really so? For a kickoff, let's start with a self\-test task\.

![0800_OutParamsCs/image1.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image1.png)

Let me remind you that _out_ parameters must be initialized by the called method before exiting it\.

Now look at the following code snippet and see if it compiles\.

```cpp
void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

_MyStruct_ \- a value type:

```cpp
public struct MyStruct
{ .... }
```

If you confidently answered "yes" or "no" \- I invite you to keep reading, since everything is not so clear\.\.\.

## Back story 

Let's start with a quick flash back\. How did we even dive into the study of _out_ parameters?

It all started with the development of another diagnostic rule for [PVS\-Studio](https://pvs-studio.com/en/pvs-studio/)\. The idea of the diagnostic is as follows \- one of the method parameters is of the _CancellationToken_ type\. This parameter is not used in the method body\. As a result, the program may not respond \(or react untimely\) to some cancellation actions, such as canceling an operation by the user's request\. When viewing warnings of the diagnostic, we found code that looks something like this:

```cpp
void Foo(out CancellationToken ct, ....)
{
  ....
  if (flag)
    ct = someValue;
  else
    ct = otherValue;
  ....
}
```

Obviously, this was a false positive, so I asked a colleague to add another unit test "with out parameters"\. He added tests, including a test of this type:

```cpp
void TestN(out CancellationToken ct)
{
  Console.WriteLine("....");
}
```

First of all, I was interested in tests with parameter initializations, but I took a closer look at this\.\.\. And then it hit me\! How does this code actually compile? Does it compile at all? The code was compiling\. Then I realized I got an article coming up\. :\)

For the sake of experiment, we decided to change the _CancellationToken_ to some other value type\. For example, _TimeSpan_:

```cpp
void TestN(out TimeSpan timeSpan)
{
  Console.WriteLine("....");
}
```

It does not compile\. Well, that's to be expected\. But why did the example with _CancellationToken_ compile?

## The out parameter modifier

Let's recall again what is a parameter's _out_ modifier\. Here are the main theses taken from docs\.microsoft\.com \([out parameter modifier](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier)\):

* The _out_ keyword causes arguments to be passed by reference;
* Variables passed as _out _arguments do not have to be initialized before being passed in a method call\. _**However, the called method is required to assign a value before the method returns\.**_

Please pay attention to the highlighted sentence\.

Here is the question\. What is the difference between the following three methods, and why does the last one compile, while the first and second do not?

```cpp
void Method1(out String obj) // compilation error
{ }

void Method2(out TimeSpan obj) // compilation error
{ }

void Method3(out CancellationToken obj) // no compilation error
{ }
```

So far, the pattern is not obvious\. Maybe there are some exceptions that are described in the docks? For the _CancellationToken_ type, for example\. Although that would be a bit strange \- what's so special about it? In the above documentation, I did not find any information about this\. Here's what the documentation suggests:_ For more information, see the [C\# Language Specification](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/introduction)\. The language specification is the definitive source for C\# syntax and usage\._

Well, let's see the specification\. We are interested in the "[Output parameters](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes)" section\. Nothing new \- it is all the same: _Every output parameter of a method must be definitively assigned before the method returns_\.

![0800_OutParamsCs/image2.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image2.png)

Well, since the official documentation and specification of the language did not give us answers, we will have to dig into the compiler\. :\)

## Exploring Roslyn

You can download the Roslyn source code [from the project page on GitHub](https://github.com/dotnet/roslyn)\. For experiments, I took the _master_ branch\. We will work with the _Compilers\.sln_ solution\. As a starting project for experiments, we use _csc\.csproj_\. You can even run it on a file with our tests to make sure that the problem is reproducible\.

For the experiments we will use the following code:

```cpp
struct MyStruct
{
  String _field;
}

void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

To check that the error really takes place, we will build and run the compiler on the file with this code\. And indeed \- the error is right there: _error CS0177: The out parameter 'obj' must be assigned to before control leaves the current method_

By the way, this message can be a good starting point for diving into the code\. The error code itself \(CS0177\) is probably generated dynamically, whereas the format string for the message is most likely somewhere in the resources\. And this is true \- we find the _ERR\_ParamUnassigned_ resource:

```cpp
<data name="ERR_ParamUnassigned" xml:space="preserve">
  <value>The out parameter '{0}' must be assigned to 
         before control leaves the current method</value>
</data>
```

By the same name, we find the error code \- _ERR\_ParamUnassigned \= 177_, as well as several places of use in the code\. We are interested in the place where the error is added \(the _DefiniteAssignmentPass\.ReportUnassignedOutParameter_ method\):

```cpp
protected virtual void ReportUnassignedOutParameter(
  ParameterSymbol parameter, 
  SyntaxNode node, 
  Location location)
{
  ....
  bool reported = false;
  if (parameter.IsThis)
  {
    ....
  }

  if (!reported)
  {
    Debug.Assert(!parameter.IsThis);
    Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, // <=
                    location, 
                    parameter.Name);
  }
}
```

Well, that seems like the place we're interested in\! We set a breakpoint and make sure that this fragment is what we need\. According to the results, _Diagnostics_ will record exactly the message that we saw:

![0800_OutParamsCs/image3.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image3.png)

Well, that's great\. And now let's change _MyStruct_ to _CancellationToken_, aaand\.\.\. We still enter this code execution branch, and the error is recorded in _Diagnostics_\. This means it's still there\! That's a twist\!

Therefore, it is not enough to track the place where the compilation error is added \- we have to explore it further\.

After some digging in the code, we go to the _DefiniteAssignmentPass\.Analyze_ method that initiated the analysis run\. The method checks, among other things, that the _out_ parameters get initialized\. In it, we find that the corresponding analysis runs 2 times:

```cpp
// Run the strongest version of analysis
DiagnosticBag strictDiagnostics = analyze(strictAnalysis: true);
....
// Also run the compat (weaker) version of analysis to see 
   if we get the same diagnostics.
// If any are missing, the extra ones from the strong analysis 
   will be downgraded to a warning.
DiagnosticBag compatDiagnostics = analyze(strictAnalysis: false);
```

There is an interesting condition below:

```cpp
// If the compat diagnostics did not overflow and we have the same 
   number of diagnostics, we just report the stricter set.
// It is OK if the strict analysis had an overflow here,
   causing the sets to be incomparable: the reported diagnostics will
// include the error reporting that fact.
if (strictDiagnostics.Count == compatDiagnostics.Count)
{
  diagnostics.AddRangeAndFree(strictDiagnostics);
  compatDiagnostics.Free();
  return;
}
```

The case is gradually becoming clearer\. We are trying to compile our code with _MyStruct_\. After strict and compat analysis we still get the same number of diagnostics that will be issued\. 

![0800_OutParamsCs/image4.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image4.png)

If we change _MyStruct_ to _CancellationToken_ in our example, _strictDiagnostics_ will contain 1 error \(as we have already seen\), and _compatDiagnostics_ will have nothing\. 

![0800_OutParamsCs/image5.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image5.png)

As a result, the above condition is not met and the method execution is not interrupted\. Where does the compilation error go? It turns out to be a simple warning:

```cpp
HashSet<Diagnostic> compatDiagnosticSet 
  = new HashSet<Diagnostic>(compatDiagnostics.AsEnumerable(), 
                            SameDiagnosticComparer.Instance);
compatDiagnostics.Free();
foreach (var diagnostic in strictDiagnostics.AsEnumerable())
{
  // If it is a warning (e.g. WRN_AsyncLacksAwaits), 
     or an error that would be reported by the compatible analysis, 
     just report it.
  if (   diagnostic.Severity != DiagnosticSeverity.Error 
      || compatDiagnosticSet.Contains(diagnostic))
  {
    diagnostics.Add(diagnostic);
    continue;
  }

  // Otherwise downgrade the error to a warning.
  ErrorCode oldCode = (ErrorCode)diagnostic.Code;
  ErrorCode newCode = oldCode switch
  {
#pragma warning disable format
    ErrorCode.ERR_UnassignedThisAutoProperty 
      => ErrorCode.WRN_UnassignedThisAutoProperty,
    ErrorCode.ERR_UnassignedThis             
      => ErrorCode.WRN_UnassignedThis,
    ErrorCode.ERR_ParamUnassigned                   // <=      
      => ErrorCode.WRN_ParamUnassigned,
    ErrorCode.ERR_UseDefViolationProperty    
      => ErrorCode.WRN_UseDefViolationProperty,
    ErrorCode.ERR_UseDefViolationField       
      => ErrorCode.WRN_UseDefViolationField,
    ErrorCode.ERR_UseDefViolationThis        
      => ErrorCode.WRN_UseDefViolationThis,
    ErrorCode.ERR_UseDefViolationOut         
      => ErrorCode.WRN_UseDefViolationOut,
    ErrorCode.ERR_UseDefViolation            
      => ErrorCode.WRN_UseDefViolation,
    _ => oldCode, // rare but possible, e.g. 
                     ErrorCode.ERR_InsufficientStack occurring in 
                     strict mode only due to needing extra frames
#pragma warning restore format
  };

  ....
  var args 
     = diagnostic is DiagnosticWithInfo { 
         Info: { Arguments: var arguments } 
       } 
       ? arguments 
       : diagnostic.Arguments.ToArray();
  diagnostics.Add(newCode, diagnostic.Location, args);
}
```

What happens in our case when using _CancellationToken_? The loop traverses _strictDiagnostics_\. Let me quickly remind you that it contains an error \- an uninitialized _out_ parameter\. _Then_ branch of the _if_ statement is not executed\. It is because _diagnostic\.Severity_ is of _DiagnosticSeverity\.Error_ value, and the  _compatDiagnosticSet_ collection is empty\. Then compilation error code is mapped with a new code \- a warning's one\. After, the warning is formed and written to the resulting collection\. This is how the compilation error turned into a warning\. :\)

By the way, it has a fairly low level\. So when you run the compiler, this warning may not be visible if you do not set the flag for issuing warnings of the appropriate level\.

Let's run the compiler and specify an additional flag: _csc\.exe %pathToFile% \-w:5_

And we see the expected warning:

![0800_OutParamsCs/image6.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image6.png)

Now we have figured out where the compilation error disappears \- it is replaced with a low\-priority warning\. However, we still do not have an answer to the question, what is the distinctiveness of _CancellationToken_ and its difference from _MyStruct_? When analyzing the method with a _MyStruct_ _out_ parameter, compat analysis finds an error\. Whereas when the parameter type is _CancellationToken_, the error can't be detected\. Why is it so?

Here I suggest grabbing a cup of tea or coffee, because we are about to get down to a painstaking investigation\.

![0800_OutParamsCs/image7.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image7.png)

I hope you took the advice and got ready\. So let's move on\. :\) 

Remember the _ReportUnassignedParameter_ method in which the compilation error was written? Let's look at the calling method above:

```cpp
protected override void LeaveParameter(ParameterSymbol parameter, 
                                       SyntaxNode syntax, 
                                       Location location)
{
  if (parameter.RefKind != RefKind.None)
  {
    var slot = VariableSlot(parameter);
    if (slot > 0 && !this.State.IsAssigned(slot))
    {
      ReportUnassignedOutParameter(parameter, syntax, location);
    }

    NoteRead(parameter);
  }
}
```

The difference when executing these methods from strict and compat analysis is that in the first case, the _slot_ variable has the value 1, and in the second \- \-1\. Therefore, in the second case, the _then_ branch of the _if_ statement is not executed\. Now we need to find out why _slot_ has the value \-1 in the second case\.

Look at the method _LocalDataFlowPass\.VariableSlot_:

```cpp
protected int VariableSlot(Symbol symbol, int containingSlot = 0)
{
  containingSlot = DescendThroughTupleRestFields(
                     ref symbol, 
                     containingSlot,                                   
                     forceContainingSlotsToExist: false);

  int slot;
  return 
    (_variableSlot.TryGetValue(new VariableIdentifier(symbol, 
                                                      containingSlot), 
                               out slot)) 
    ? slot 
    : -1;
}
```

In our case, _\_variableSlot_ does not contain a slot for the _out_ parameter\. Therefore,  _\_variableSlot\.TryGetValue\(\.\.\.\.\) _returns _false_\. The code execution follows the alternative branch of the ?:, operator, and the method returns \-1\. Now we need to understand why _\_variableSlot_ does not contain an _out_ parameter\.

![0800_OutParamsCs/image8.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image8.png)

After digging around, we find the _LocalDataFlowPass\.GetOrCreateSlot_ method\. It looks like this:

```cpp
protected virtual int GetOrCreateSlot(
  Symbol symbol, 
  int containingSlot = 0, 
  bool forceSlotEvenIfEmpty = false, 
  bool createIfMissing = true)
{
  Debug.Assert(containingSlot >= 0);
  Debug.Assert(symbol != null);

  if (symbol.Kind == SymbolKind.RangeVariable) return -1;

  containingSlot 
    = DescendThroughTupleRestFields(
        ref symbol, 
        containingSlot,
        forceContainingSlotsToExist: true);

  if (containingSlot < 0)
  {
    // Error case. Diagnostics should already have been produced.
    return -1;
  }

  VariableIdentifier identifier 
    = new VariableIdentifier(symbol, containingSlot);
  int slot;

  // Since analysis may proceed in multiple passes, 
     it is possible the slot is already assigned.
  if (!_variableSlot.TryGetValue(identifier, out slot))
  {
    if (!createIfMissing)
    {
      return -1;
    }

    var variableType = symbol.GetTypeOrReturnType().Type;
    if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
    {
      return -1;
    }

    if (   _maxSlotDepth > 0 
        && GetSlotDepth(containingSlot) >= _maxSlotDepth)
    {
      return -1;
    }

    slot = nextVariableSlot++;
    _variableSlot.Add(identifier, slot);
    if (slot >= variableBySlot.Length)
    {
      Array.Resize(ref this.variableBySlot, slot * 2);
    }

    variableBySlot[slot] = identifier;
  }

  if (IsConditionalState)
  {
    Normalize(ref this.StateWhenTrue);
    Normalize(ref this.StateWhenFalse);
  }
  else
  {
    Normalize(ref this.State);
  }

  return slot;
}
```

The method shows that there is a number of conditions when the method returns \-1, and the slot will not be added to _\_variableSlot_\. If there is no slot for a variable yet, and all checks are successful, then an entry is made in _\_variableSlot_: _\_variableSlot\.Add\(identifier, slot\)_\. We debug the code and see that when performing strict analysis, all checks pass successfully\. Whereas when performing compat analysis, we finish executing the method in the following _if_ statement:

```cpp
var variableType = symbol.GetTypeOrReturnType().Type;
if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
{
  return -1;
}
```

The value of the _forceSlotEvenIfEmpty_ variable is _false_ in both cases\. The difference is in the value of the _IsEmptyStructType_ method: for strict analysis it is _false_, for compat analysis – _true_\.

![0800_OutParamsCs/image9.png](https://import.viva64.com/docx/blog/0800_OutParamsCs/image9.png)

At this point I already have new questions and the desire to do some experiments\. So it turns out that if the type of the_ out _parameter is an "empty structure" \(later we will get what this means\), the compiler considers such code valid and does not generate an error, right? In our example, we remove the field from _MyStruct_ and compile it\.

```cpp
struct MyStruct
{  }

void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

And this code compiles successfully\! Interesting\.\.\. I can't recall any mention of such features in the documentation and specification\. :\)

Here comes another question: how does the code work when the type of the _out _parameter is _CancellationToken_? After all, this is clearly not an "empty structure"\. If you check out the code at referencesource\.microsoft\.com \([link to CancellationToken](https://referencesource.microsoft.com/)\), it becomes clear that this type contains methods, properties, and fields\.\.\. Still not clear, let's keep digging\.

Let's go back to the _LocalDataFlowPass\.IsEmptyStructType_ _method_: 

```cpp
protected virtual bool IsEmptyStructType(TypeSymbol type)
{
  return _emptyStructTypeCache.IsEmptyStructType(type);
}
```

Let's go deep \(_EmptyStructTypeCache\.IsEmptyStructType_\):

```cpp
public virtual bool IsEmptyStructType(TypeSymbol type)
{
  return IsEmptyStructType(type, ConsList<NamedTypeSymbol>.Empty);
}
```

And even deeper:

```cpp
private bool IsEmptyStructType(
  TypeSymbol type, 
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType)
{
  var nts = type as NamedTypeSymbol;
  if ((object)nts == null || !IsTrackableStructType(nts))
  {
    return false;
  }

  // Consult the cache.
  bool result;
  if (Cache.TryGetValue(nts, out result))
  {
    return result;
  }

  result = CheckStruct(typesWithMembersOfThisType, nts);
  Debug.Assert(!Cache.ContainsKey(nts) || Cache[nts] == result);
  Cache[nts] = result;

  return result;
}
```

The code is executed by calling the _EmptyStructTypeCache\.CheckStruct_ method:

```cpp
private bool CheckStruct(
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType, 
  NamedTypeSymbol nts)
{
  .... 
  if (!typesWithMembersOfThisType.ContainsReference(nts))
  {
    ....
    typesWithMembersOfThisType 
      = new ConsList<NamedTypeSymbol>(nts, 
                                      typesWithMembersOfThisType);
    return CheckStructInstanceFields(typesWithMembersOfThisType, nts);
  }

  return true;
}
```

Here, the execution goes into _then _branch of the _if_ statement, as the _typesWithMembersOfThisType_ collection is empty\. Check out the _EmptyStructTypeCache\.IsEmptyStructType_ method, where it is passed as an argument\. 

We're getting some clarity here \- now we understand what is an "empty structure"\. Judging by the methods' names, this is a structure that does not contain instance fields\. But let me remind you that there are instance fields in _CancellationToken_\. So, we go the extra mile and check out the _EmptyStructTypeCache\.CheckStructInstanceFields_ method\.

```cpp
private bool CheckStructInstanceFields(
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType, 
  NamedTypeSymbol type)
{
  ....
  foreach (var member in type.OriginalDefinition
                             .GetMembersUnordered())
  {
    if (member.IsStatic)
    {
      continue;
    }
    var field = GetActualField(member, type);
    if ((object)field != null)
    {
      var actualFieldType = field.Type;
      if (!IsEmptyStructType(actualFieldType, 
                             typesWithMembersOfThisType))
      {
        return false;
      }
    }
  }

  return true;
}
```

The method iterates over instance members\. We get 'actualField' for each of them\. We managed to get this value \(_field_ \- not _null_\) and next we check if the type of this field is an "empty structure"\. This means if we find at least one "non\-empty structure", we also consider the original type to be a "non\-empty structure"\. If all the instance fields are "empty structures", then the original type is also considered an "empty structure"\.

We'll have to go a little deeper\. Don't worry, our dive will be over soon, and we'll put the dots on the 'i'\. :\)

Look at the method _EmptyStructTypeCache\.GetActualField_:

```cpp
private FieldSymbol GetActualField(Symbol member, NamedTypeSymbol type)
{
  switch (member.Kind)
  {
    case SymbolKind.Field:
      var field = (FieldSymbol)member;
      ....
      if (field.IsVirtualTupleField)
      {
        return null;
      }

      return (field.IsFixedSizeBuffer || 
              ShouldIgnoreStructField(field, field.Type)) 
            ? null 
            : field.AsMember(type);

      case SymbolKind.Event:
        var eventSymbol = (EventSymbol)member;
        return (!eventSymbol.HasAssociatedField || 
               ShouldIgnoreStructField(eventSymbol, eventSymbol.Type)) 
             ? null 
             : eventSymbol.AssociatedField.AsMember(type);
  }

  return null;
}
```

Accordingly, for the _CancellationToken_ type, we are interested in _the_ _SymbolKind\.Field_ _case_\-branch\. We can only get into it when analyzing the _m\_source_ member of this type\. It is because the _CancellationToken _type contains only one instance field – _m\_source_\)\.

Let's look at calculations in this _case_ \(branch in our case\)\.

_field\.IsVirtualTupleField_ \- _false_\. We move on to the conditional operator and parse the conditional expression _field\.IsFixedSizeBuffer \|\| ShouldIgnoreStructField\(field, field\.Type\)_\. _field\.IsFixedSizeBuffer_ is not our case\. As expected the value is _false_\. As for the value returned by calling the _ShouldIgnoreStructField\(field, field\.Type\)_ method, it differs for strict and compat analysis\. A quick reminder – we analyze the same field of the same type\.

Here is the body of the _EmptyStructTypeCache\.ShouldIgnoreStructField_ method:

```cpp
private bool ShouldIgnoreStructField(Symbol member, 
                                     TypeSymbol memberType)
{
  // when we're trying to be compatible with the native compiler, we 
     ignore imported fields (an added module is imported)
     of reference type (but not type parameters, 
     looking through arrays)
     that are inaccessible to our assembly.

  return _dev12CompilerCompatibility &&                             
         ((object)member.ContainingAssembly != _sourceAssembly ||   
          member.ContainingModule.Ordinal != 0) &&                      
         IsIgnorableType(memberType) &&                                 
         !IsAccessibleInAssembly(member, _sourceAssembly);          
}
```

Let's see what is different for strict and compat analysis\. Well, you may have already guessed on your own\. :\)

Strict analysis:_\_dev12CompilerCompatibility_ – _false_, hence the result of the entire expression is _false_\. Compat analysis: the values of all subexpressions are _true_; the result of the entire expression is _true_\. 

And now we follow the chain of conclusions, rising to the top from the very end\. :\)

In compat analysis, we think that we should ignore a single instance field of the _CancellationSource_ type, which is _m\_source_\. Thus, we decided that _CancellationToken_ is an "empty structure", hence no slot is created for it, and no "empty structures" are written to the cache\. Since there is no slot, we do not process the _out_ parameter and do not record a compilation error when performing compat analysis\. As a result, strict and compat analysis give different results, which is why the compilation error is downgraded to a low\-priority warning\.

That is, this is not some special processing of the _CancellationToken_ type\. There is a number of types for which the lack of _out_ parameter's initialization will not lead to compilation errors\.

Let's try to see in practice which types will be successfully compiled\. As usual, we take our typical method:

```cpp
void CheckYourself(out MyType obj)
{
  // Do nothing
}
```

And try to substitute different types instead of _MyType_\. We've already figured out that this code compiles successfully for _CancellationToken_ and for an empty structure\. What else?

```cpp
struct MyStruct
{ }

struct MyStruct2
{
  private MyStruct _field;
}
```

If we use_ MyStruct2_ instead of _MyType_, the code also compiles successfully\.

```cpp
public struct MyExternalStruct
{
  private String _field;
}
```

When using this type, the code will compile successfully if _MyExternalStruct_ is declared in an external assembly\. If _MyExternalStruct_ is declared in the same assembly with the _CheckYourself_ method, it does not compile\.

When using this type from an external assembly, the code no longer compiles, as we changed the access modifier of the _\_field_ field from _private_ to _public_:

```cpp
public struct MyExternalStruct
{
  public String _field;
}
```

With this kind of change, the code will not compile either, since we changed the field type from _String_ to _int_:

```cpp
public struct MyExternalStruct
{
  private int _field;
}
```

As you may have guessed, there is a certain scope for experimentation\.

## Let's recap 

Generally speaking, _out_ parameters must be initialized before the called method returns control to the caller\. However, as practice shows, the compiler can make its own adjustments to this requirement\. In some cases, a low\-level warning will be issued instead of a compilation error\. Why exactly this happens, we discussed in detail in the previous section\.

But what about the types for which you can skip initializing _out_ parameters? For example, parameter initialization is not required if the type is a structure with no fields\. Or if all fields are structures with no fields\. Here is the case with _CancellationToken_\. This type is in the external library\. Its only  _m\_source_ field is of a reference type\. The field itself is not available from external code\. By these reasons the compilation is successful\. Well, you can come up with other similar types \- you'll be able to not initialize _out_ parameters and successfully compile your code\.

Going back to the question from the beginning of the article:

```cpp
void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
public struct MyStruct
{ .... }
```

Does this code compile? As you have already understood, neither 'Yes' nor 'No' is the correct answer\. Depending on what _MyStruct_ is, what fields are there, where the type is declared, etc\. – this code can either compile or not compile\.

## Conclusion

What we went through today is diving into the compiler's source code to answer a seemingly simple question\. I think we will repeat this experience soon, as the topic for the next similar article is already there\. Stay in touch\. ;\)

By the way, subscribe to [my Twitter account](https://twitter.com/_SergVasiliev_), where I also post articles and other interesting findings\. This way you won't miss anything exciting\. :\)