6

Consider the following code:

private unsafe void Function()
{
    int length;

    // This line raises error CS1686, "Local 'length' or its members cannot have their address taken and be used inside an anonymous method or lambda expression".
    glGetProgramiv(1, GL_PROGRAM_BINARY_LENGTH, &length);

    FunctionWithLambda(() => Console.WriteLine(length));
}

private void FunctionWithLambda(Action callback)
{
    callback();
}

Note that I'm taking the address of length (a local variable), then using the variable itself (not its address) in a lambda. I understand why a local variable address can't be used in a lambda directly (see Why cannot I pass the address of a variable to an anonymous function?, among other examples), but why can't I use the value of length once assigned (even if that assignment happens to use the & operator)? The official documentation for error CS1686 (https://learn.microsoft.com/bs-latn-ba/dotnet/csharp/misc/cs1686) hasn't clarified this confusion.

My assumption is that this is simply a language limitation, but I'm curious if there's an underlying technical reason I'm missing. Also note I'm not asking how to work around this problem (I know I can easily copy length to another local variable first).

Charlieface
  • 52,284
  • 6
  • 19
  • 43
Grimelios
  • 351
  • 2
  • 11
  • You know this isn't a problem if you're not using `unsafe` code, right? – StriplingWarrior Dec 07 '21 at 23:49
  • Yes, but in this case, I need unsafe case. – Grimelios Dec 07 '21 at 23:51
  • Does the answer in the SO post you reference not apply still? `glGetProgramiv` could store the pointer someplace and later deallocate it, and the compiler would not be able to make sure that the memory address is still valid when the delegate uses it? – StriplingWarrior Dec 08 '21 at 00:03
  • 1
    Usually, questions asking "why" on this level ends up being closed because at the very core of most of these reasons, you're asking for the opinion of those people that sat in the design meeting where they decided on this. Apart from the C# specification and more recent GitHub issue discussions, the core decisions aren't validated anywhere we have access to so whatever most people here will answer is going to be a guess at best. A few people might know, but the chance of them being here and piping in is slim. – Lasse V. Karlsen Dec 08 '21 at 11:28
  • out of interest what happens if you assign the value to a separate int after the gl call and use that in your lambda instead? – GazTheDestroyer Dec 08 '21 at 11:33
  • 3
    The C# 4 specification states: "When a local variable or a value parameter is captured by an anonymous function, the local variable or parameter is no longer considered to be a fixed variable (§18.3), but instead is considered to be a moveable variable. Thus any unsafe code that takes the address of a captured outer variable must first use the fixed statement to fix the variable." **It may be just a case of the wrong error message being given** – Lasse V. Karlsen Dec 08 '21 at 11:39
  • @LasseV.Karlsen intersting. But then you'd need to box `length`, I believe, because `CS0213: You cannot use the fixed statement to take the address of an already fixed expression`. – Good Night Nerd Pride Dec 08 '21 at 13:03

2 Answers2

1

The C# specification says the following (my bold):

23.4 Fixed and moveable variables

The address-of operator (§23.6.5) and the fixed statement (§23.7) divide variables into two categories:
Fixed variables and moveable variables.

...snip...

The & operator (§23.6.5) permits the address of a fixed variable to be obtained without restrictions. However, because a moveable variable is subject to relocation or disposal by the garbage collector, the address of a moveable variable can only be obtained using a fixed statement (§23.7), and that address remains valid only for the duration of that fixed statement.

In precise terms, a fixed variable is one of the following:

  • A variable resulting from a simple-name (§12.7.3) that refers to a local variable, value parameter, or parameter array, unless the variable is captured by an anonymous function (§12.16.6.2).
  • .....

So it's explicitly forbidden by the spec. As to why it's forbidden, for that you would have to ask the language designers, but considering how much complexity is involved in capturing variables, it is somewhat logical.

Charlieface
  • 52,284
  • 6
  • 19
  • 43
0

I guess the reason is simple: Too complex to compile.

There are 2 problems the compiler has to solve:

  1. Generate a clourse for the anonymous method.
  2. Synchronize the value of the variable.

Let's assume the following codes are valid.

unsafe void Function()
{
    int length = 1;
    void bar() => Console.WriteLine(length);
    bar();
    foo(&length);
    bar();
}

unsafe void foo(int* i) { (*i)++; }

Expected result is:

1
2

To solve the first problem C# will generate an anonymous class to hold the upvalue.

Here is the pseudocode:

class _Anonymous
{
    public int _length;

    public void _bar() { Console.WriteLine(_length); }
}

unsafe void Function()
{
    int length = 1;
    var a = new _Anonymous { _length = length };
    a._bar();
    foo(&length);
    a._bar();
}

To solve the second problem C# uses the generated field instead of the original local variable.

unsafe void Function()
{
    //int length = 1;
    var a = new _Anonymous { _length = 1 };
    a._bar();
    foo(&a._length);
    a._bar();
}

These are all the works that a compiler can do. But till now the codes still won't work, we need an extra fixed block.

unsafe void Function()
{
    var a = new _Anonymous { _length = 1 };
    a._bar();
    fixed (int* p = &a._length)
        foo(p);
    a._bar();
}

So the limitation can be removed with a smarter compiler, but things get more easy if we forbid such kind of codes.

shingo
  • 18,436
  • 5
  • 23
  • 42