"k + = c + = k + = c;"에 인라인 연산자에 대한 설명이 있습니까?


89

다음 작업의 결과에 대한 설명은 무엇입니까?

k += c += k += c;

다음 코드의 출력 결과를 이해하려고했습니다.

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

현재 "k"의 결과가 80 인 이유를 이해하는 데 어려움을 겪고 있습니다. k = 40 할당이 작동하지 않는 이유는 무엇입니까 (실제로 Visual Studio는 해당 값이 다른 곳에서 사용되지 않는다고 알려줍니다)?

왜 110이 아닌 k 80입니까?

작업을 다음으로 분할하면 :

k+=c;
c+=k;
k+=c;

결과는 k = 110입니다.

CIL 을 살펴 보려고 했지만 생성 된 CIL을 해석하는 데 그다지 심오하지 않으며 몇 가지 세부 정보를 얻을 수 없습니다.

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????

3
함수를 분할했기 때문에 다른 결과를 얻었습니다. k + = c + = k + = c = 80 k와 c의 값이 모든 합에서 동일하게 유지되므로 k + = c + = k + = c가 동일합니다. 10 + 30 + 10 + 30
주앙 파올로 아모

78
흥미로운 운동이지만 실제로는 동료가 당신을 싫어하는 것을 원하지 않는 한 그런 코드 체인을 작성하지 마십시오. :)
UnhandledExcepSean

3
@AndriiKotliarov 왜냐하면 k + = c + = k + = c는 10 + 30 + 10 + 30이므로 K는 모든 값을 받고 C는 마지막 3 개의 인수 만 가져옵니다 30 + 10 + 30 = 70
João Paulo Amorim

6
또한 가치가 읽기 - 에릭 Lippert의의 대답의 I의 차이 ++와 ++ i가 무엇입니까?
Wai Ha Lee

34
"의사 님, 박사님, 이렇게하면 아파요!" "그렇게하지 마십시오."
David Conrad

답변:


104

같은 작업 a op= b;a = a op b;. 할당은 문이나 식으로 사용할 수 있지만 식으로 할당 된 값을 생성합니다. 귀하의 진술 ...

k += c += k += c;

... 할당 연산자는 오른쪽 연관이므로 다음과 같이 작성할 수도 있습니다.

k += (c += (k += c));

또는 (확장 됨)

k =  k +  (c = c +  (k = k  + c));
     10301030   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   4010 + 30   // operator evaluation7030 + 40
8010 + 70

전체 평가 중에 관련된 변수의 이전 값이 사용되는 곳. 이것은 특히 가치에 대해 사실입니다 k(아래 IL에 대한 리뷰 및 Wai Ha Lee가 제공 한 링크 참조 ). 따라서 70 + 40 (새 값 k) = 110이 아니라 70 + 10 (이전 값 k) = 80이됩니다.

요점은 (C # 사양 에 따라 ) "표현식 의 피연산자는 왼쪽에서 오른쪽으로 평가됩니다" (피연산자는 변수 c이며 k우리의 경우)입니다. 이것은이 경우 오른쪽에서 왼쪽으로 실행 순서를 지시하는 연산자 우선 순위 및 연관성과는 무관합니다. ( 이 페이지에서 Eric Lippert의 답변대한 의견을 참조 하십시오).


이제 IL을 살펴 보겠습니다. IL은 스택 기반 가상 머신을 가정합니다. 즉, 레지스터를 사용하지 않습니다.

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

이제 스택은 다음과 같습니다 (왼쪽에서 오른쪽으로, 스택의 맨 위가 오른쪽).

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

참고 IL_000c: dup, IL_000d: stloc.0즉로 첫 임무 k , 멀리 최적화 할 수 있습니다. 아마도 이것은 IL을 기계어 코드로 변환 할 때 지터에 의해 변수에 대해 수행됩니다.

또한 계산에 필요한 모든 값은 할당되기 전에 스택에 푸시되거나 이러한 값에서 계산됩니다. (기준 할당 된 값 stloc)이 평가하는 동안 다시 사용되지 않습니다. stloc스택의 맨 위를 팝니다.


다음 콘솔 테스트의 출력은 ( Release최적화 가 설정된 모드)입니다.

k 평가 (10)
c 평가 (30)
k 평가 (10) k
평가 (30)
40 k에
할당 70 c에
할당 80 k에 할당

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

보다 완전한 공식에 숫자를 사용하여 최종 결과를 추가 할 수 있습니다. final is k = 10 + (30 + (10 + 30)) = 80c최종 값은 첫 번째 괄호 안에 설정됩니다 c = 30 + (10 + 30) = 70.
Franck

2
실제로 k가 로컬이면 최적화가 켜져 있으면 죽은 저장소가 거의 확실히 제거되고 그렇지 않으면 보존됩니다. 흥미로운 질문은 필드, 속성, 배열 슬롯 등의 경우 지터가 데드 스토어를 제거 할 있는지 여부 k입니다. 실제로는 그렇지 않다고 생각합니다.
Eric Lippert

릴리스 모드의 콘솔 테스트는 실제로 k속성 인 경우 두 번 할당 되었음을 보여줍니다 .
Olivier Jacot-Descombes 2019

26

우선, Henk와 Olivier의 대답이 맞습니다. 조금 다른 방식으로 설명하고 싶습니다. 특히이 점을 말씀 드리고 싶습니다. 다음과 같은 문장이 있습니다.

int k = 10;
int c = 30;
k += c += k += c;

그런 다음이 명령문 세트와 동일한 결과를 제공해야한다고 잘못 결론을 내립니다.

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

당신이 어떻게 잘못했는지, 그리고 그것을 올바르게하는 방법을 보는 것은 유익합니다. 그것을 분해하는 올바른 방법은 다음과 같습니다.

먼저 가장 바깥 쪽 + =를 다시 작성하십시오.

k = k + (c += k += c);

둘째, 가장 바깥 쪽 +를 다시 작성하십시오. x = y + z는 항상 "y를 임시로 평가하고, z를 임시로 평가하고, 임시를 합하고, 합계를 x에 할당"과 동일해야한다는 데 동의하기를 바랍니다 . 그래서 그것을 매우 명시 적으로 만들어 보겠습니다.

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

이것은 당신이 잘못한 단계이기 때문에 명확하게 확인하십시오 . 복잡한 작업을 더 간단한 작업으로 나눌 때 천천히 조심스럽게 수행 하고 단계를 건너 뛰지 않도록해야합니다 . 단계를 건너 뛰는 것은 우리가 실수하는 곳입니다.

이제 t2에 대한 할당을 다시 천천히 조심스럽게 분해합니다.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

할당은 c에 할당 된 것과 동일한 값을 t2에 할당하므로 다음과 같이 가정 해 보겠습니다.

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

큰. 이제 두 번째 줄을 나눕니다.

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

좋습니다. 우리는 발전하고 있습니다. t4에 대한 할당을 분류합니다.

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

이제 세 번째 줄을 나눕니다.

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

이제 우리는 모든 것을 볼 수 있습니다.

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

완료되면 k는 80이고 c는 70입니다.

이제 이것이 IL에서 어떻게 구현되는지 살펴 보겠습니다.

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

이제 이것은 약간 까다 롭습니다.

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

위의 내용을 다음과 같이 구현할 수 있습니다.

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

그러나 "dup"트릭을 사용하면 코드가 짧아지고 지터가 더 쉬워지기 때문에 동일한 결과를 얻습니다. 일반적으로 C # 코드 생성기는 스택에서 가능한 한 임시를 "임시"로 유지하려고합니다. 일시적인 횟수를 줄 이면서 IL을 따르는 것이 더 쉽다면 최적화를 끄면 코드 생성기가 덜 공격적입니다.

이제 c를 얻기 위해 동일한 트릭을 수행해야합니다.

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

그리고 마지막으로:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

우리는 다른 것에 대한 합계가 필요하지 않기 때문에 그것을 복제하지 않습니다. 이제 스택이 비어 있으며 명령문의 끝입니다.

이야기의 교훈은 다음과 같습니다. 복잡한 프로그램을 이해하려고 할 때 항상 한 번에 하나씩 작업을 세분화하십시오 . 지름길을 사용하지 마십시오. 그들은 당신을 타락하게 할 것입니다.


3
@ OlivierJacot-Descombes : 사양 관련 광고는 "운영자"을 왼쪽에서 오른쪽으로 식에서 피연산자 평가 "라는 부분에 예를 들면,. F(i) + G(i++) * H(i), 방법 F I의 이전 값을 사용하여 호출 된 다음 메소드 G는 i의 이전 값으로 호출되고 마지막으로 메소드 H는 i의 새 값으로 호출됩니다 . 이는 연산자 우선 순위와는 별개이며 관련이 없습니다. " (강조가 추가되었습니다.) 그래서 "이전 가치가 사용된다"는 곳이 없다고 말했을 때 제가 틀렸던 것 같아요! 예에서 발생합니다. 그러나 규범적인 부분은 "왼쪽에서 오른쪽으로"입니다.
Eric Lippert

1
이것은 누락 된 링크였습니다. 본질은 피연산자 평가 순서와 연산자 우선 순위를 구분해야 한다는 입니다. 피연산자 평가는 왼쪽에서 오른쪽으로, OP의 경우 연산자 실행은 오른쪽에서 왼쪽으로 진행됩니다.
Olivier Jacot-Descombes 2019

4
@ OlivierJacot-Descombes : 맞습니다. 우선 순위와 연관성은 하위 표현식 경계가 어디에 있는지를 결정 한다는 사실 외에는 하위 표현식이 평가되는 순서 와 관련이 없습니다 . 하위 표현식은 왼쪽에서 오른쪽으로 평가됩니다.
Eric Lippert

1
죄송합니다. 할당 연산자를 오버로드 할 수없는 것 같습니다. /
johnny 5

1
@ johnny5 : 맞습니다. 그러나 당신은 오버로드 할 수 있으며 +, 예외 는 한 번만 평가 되는 것으로 정의되어 있으므로 +=무료로 얻을 수 있습니다. 기본 제공 또는 사용자 정의 여부에 관계없이 사실 입니다. 따라서 참조 유형에 오버로딩 을 시도 하고 어떤 일이 발생하는지 확인하십시오. x += yx = x + yx++
Eric Lippert

14

이것은 다음과 같이 요약됩니다. +=원본에 가장 먼저 적용 k되는가 아니면 오른쪽으로 더 많이 계산 된 값에 적용 됩니까?

대답은 할당이 오른쪽에서 왼쪽으로 바인딩되지만 작업은 여전히 ​​왼쪽에서 오른쪽으로 진행된다는 것입니다.

그래서 가장 왼쪽 +=10 += 70.


1
이것은 너트 쉘에 멋지게 넣습니다.
Aganju

실제로 왼쪽에서 오른쪽으로 평가되는 피연산자입니다.
Olivier Jacot-Descombes 2019

0

나는 gcc와 pgcc로 예제를 시도했고 110을 얻었다. 나는 그들이 생성 한 IR을 확인했고 컴파일러는 expr을 다음과 같이 확장했다.

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

나에게 합리적으로 보입니다.


-1

이러한 종류의 체인 할당의 경우 가장 오른쪽에서 시작하여 값을 할당해야합니다. 할당하고 계산하고 왼쪽에 할당해야하며 마지막 (가장 왼쪽 할당)까지 계속 진행해야합니다. 물론 k = 80으로 계산됩니다.


다른 수많은 답변이 이미 언급 한 내용을 단순히 다시 언급하는 답변을 게시하지 마십시오.
Eric Lippert

-1

간단한 대답 : vars를 값으로 바꾸면 얻을 수 있습니다.

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

이 대답은 틀 렸습니다. 이 기술은이 특정 경우에 작동하지만 해당 알고리즘은 일반적으로 작동하지 않습니다. 예를 들어 k = 10; m = (k += k) + k;은를 의미하지 않습니다 m = (10 + 10) + 10. 변형식이있는 언어는 열망 값 대체가있는 것처럼 분석 할 수 없습니다 . 값 대체 는 돌연변이와 관련하여 특정 순서로 발생하며 이를 고려해야합니다.
Eric Lippert

-1

세어 보면 해결할 수 있습니다.

a = k += c += k += c

두 개의 cs와 두 개의 ks가 있으므로

a = 2c + 2k

그리고 언어 연산자의 결과로 다음과 k같습니다.2c + 2k

이것은이 체인 스타일의 변수 조합에 대해 작동합니다.

a = r += r += r += m += n += m

그래서

a = 2m + n + 3r

그리고 r동일합니다.

가장 왼쪽 할당까지만 계산하여 다른 숫자의 값을 계산할 수 있습니다. 그래서 m동일 2m + nn같습니다 n + m.

이것은 그것이 k += c += k += c;다른 k += c; c += k; k += c;이유와 다른 대답을 얻는 이유를 보여줍니다 .

댓글의 일부 사람들은이 단축키에서 가능한 모든 유형의 추가로 지나치게 일반화하려고 할 수 있다고 걱정하는 것 같습니다. 따라서이 단축키는이 상황에만 적용 할 수 있음을 분명히 할 것입니다. 즉, 내장 된 숫자 유형에 대한 추가 할당을 함께 연결하는 것입니다. 예를 들어 ()또는 에 다른 연산자를 추가 +하거나 함수를 호출하거나 재정의 한 +=경우 또는 기본 숫자 유형이 아닌 다른 것을 사용하는 경우 에는 (필연적으로) 작동하지 않습니다 . 질문의 특정 상황에 도움을주기위한 것 입니다.


이것은 질문에 대한 대답이 아닙니다
johnny 5

@ johnny5 그것은 당신이 얻는 결과를 얻는 이유를 설명합니다. 즉, 그것이 수학이 작동하는 방식이기 때문입니다.
Matt Ellen

2
컴파일러가 명령문을 평가하는 수학 및 연산 순서는 서로 다른 두 가지입니다. 당신의 논리에 따라 k + = c; c + = k; k + = c는 동일한 결과로 평가되어야합니다.
johnny 5

아니, 조니 5, 그건 의미가 아니야 수학적으로 그들은 다른 것들입니다. 세 개의 개별 연산은 3c + 2k로 평가됩니다.
Matt Ellen

2
불행히도 "대수적"해법은 우연히 일치 할 뿐입니다 . 귀하의 기술은 일반적으로 작동하지 않습니다 . 고려 x = 1;y = (x += x) + x;그것은 당신의 주장은 "세 X의가 그래서 Y는 같다 3 * x?" 때문에 yIS 같 4이 경우. 이제 y = x + (x += x);대수 법칙 "a + b = b + a"가 충족되고 이것도 4라는 당신의 주장은 어떻습니까? 왜냐하면 이것은 3이기 때문입니다. 불행히도 C #은 식에 부작용이있는 경우 고등학교 대수의 규칙을 따르지 않습니다 . C #은 부작용 대수의 규칙을 따릅니다.
Eric Lippert
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.