推 LiloHuang: 優先權是指誰先做誰後做,但是該做的事情還是都會執行 06/28 12:58
這裡其實我覺得優先權不是指誰先做誰後做,而是指一個運算式該怎麼被我們解讀
(後面修文補完文章內容)
--
※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 36.225.46.73
※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1435482334.A.2D2.html
→ suhorng: 不小心按錯發文了QQ 還沒寫完, 可以麻煩板主幫刪嗎.. 06/28 17:06
→ ssdoz2sk: 在文章標題上按大寫E可以修改內文 標題可以用大寫T 06/28 18:08
推 LiloHuang: 我意指像是 +-*/ 運算子,我們會知道先乘除後加減 06/28 19:14
→ LiloHuang: 對於運算子的優先權解讀,就是會有誰先做誰後做的順序 06/28 19:15
→ LiloHuang: 我猜測原po認為優先權較大的情況,可能某運算子被忽略 06/28 19:16
→ LiloHuang: 在 flat expression 裡 Precedence 用以定義執行順序 06/28 19:27
→ LiloHuang: 應該有更加嚴謹的定義吧 06/28 19:45
推 LPH66: 你想要的「更加嚴謹的定義」很多人都回了, 就是如何解讀 06/28 20:11
→ LPH66: 只是在沒有副作用的時候它就是運算順序而已 06/28 20:12
→ LPH66: 這裡因為 ++ 是有副作用的運算子所以才會有這種差別 06/28 20:12
→ suhorng: 今天有事沒法回文了...明天再來修完 既然有推文了文章還 06/28 20:14
→ suhorng: 是留著好了XD 06/28 20:15
推 LiloHuang: postfix ++ operator 有 side effect 這我知道 XD 06/28 20:34
→ LiloHuang: 我上一篇的推文,是假設原 po 已知 side effect 的情況 06/28 20:35
→ LiloHuang: 提及運算子優先權,決定了編譯後生成的機器碼順序 06/28 20:36
→ LiloHuang: 畢竟運算子優先權的定義,就是決定了誰該先做誰該後做 06/28 20:38
→ LiloHuang: 事後想想,side effect 的部分才是最初原 po 要的答案 06/28 20:39
→ LiloHuang: 但是我的疑惑是,暫時撇開 postfix ++ 的 side effect 06/28 20:46
→ LiloHuang: 為什麼運算子優先權,suhorng 認為不是先後執行順序 06/28 20:47
→ suhorng: 有三個運算子在 "優先權 => 運算先後" 的命題下會變例外 06/28 21:20
→ suhorng: &&, ||, ? : 06/28 21:20
→ suhorng: 另外 sizeof 應該也可以算是一個 operator @@ 06/28 21:22
推 LiloHuang: 謝謝,這樣我懂你的意思了,的確用 "如何解讀" 較恰當 06/28 21:29
→ LiloHuang: sizeof 的確也是一個 operator 沒錯 XD 06/28 21:29
講更詳細一點, 我喜歡的 approach (尤其適用於程式語言)
對於一個算式 我們心理想的其實是一棵樹(Abstract Syntax Tree, AST),
為了寫作方便, 寫成平的算式, 並用括弧和 operator precedence, grammar 等等
來建立一個平的字串跟這棵樹之間的關係.
+ λ
/ \ / \
1 * x @
/ \ / \
2 3 x x
1 + 2 * 3 λx. x x
而對於這棵樹有什麼 "意義", 是由我們定義決定的. 最簡單的方式是定義一個解釋函數
value_t eval(tree_t expr) {
if (expr == "+") {
return intval( eval(expr.left).intval() + eval(expr.right).intval() );
} else if (expression == "*") {
...
}
}
而當然 eval 函數會適當的決定求值順序以及好好的解釋副作用等等, 例如
...
if (expr == "?:") {
val ret = eval(expr.first);
if (res.boolval() == true)
return eval(expr.second);
else
return eval(expr.third);
} else if (expr == "&&") {
val ret = expr(expr.left);
if (ret.boolval() == true)
return eval(expr.right);
else
return false;
}
...
等等. 我們去看一般語言的定義時, 通常語意都是遞歸的定義在這棵語法樹上,
很少有直接對平平的算式做討論. 當然, 考慮普通的四則運算加減乘除, 由於
+-*/ 這幾個運算子都是 strict, 要說這導致了 +-*/ 的優先權決定了運算順序,
當然不能說是錯的. 但這畢竟是難以一般化的特例. 舉例來說, 函數呼叫其實也
可以是個 operator (我不是說 operator() ), 但函數呼叫要不要把參數先算完
是可以有不同的選擇.
回到 C++. 目前的 C++ 描述的方式是設計 value computation 跟 side effect
兩種. value computation 描述一個值的計算, 而 side effect, 副作用, 一般來說
就是做一些其他的事情像變數修改, 做 I/O 等等.
對於運算子及函數呼叫, C++ 規定了一套該運算子本身的運算和它的參數們之間
value computation, side effect 的偏序關係. 有受到偏序關係規範的我們能說
他們之間誰會先做誰會後做, 未受偏序關係規範的則還有分有可能交替著做, 或者
只能是誰先誰後(不能重疊)等等.
最簡單的例子來說, + 就規定其左右運算元的 value computation 要在 + 的
value computation 之前作完, 不過對於 side effects 則沒有規範. 當兩個
未受偏序關係規制的 side effects 影響到同一個物件時, 就是未定義行為.
回到最一開始的例子, 一個算式如 f1(g()) + f2(h(z(), w())) * f3() 當中,
其實我們副作用發生的順序根本是很寬鬆的定義的. 我們只知道 z,w 的副作用
發生在 h 之前, h 的副作用發生在 f2 之前, g 的副作用發生在 f1 之前,
其餘副作用是有可能任意穿插發生的. 在這種例子中我們很難說優先權就決定了
運算順序. 在副作用之外, 甚至在運算式 e1 + e2 + e3 當中, 三個運算元的
計算順序根本沒有確定規則. 這個算式依據優先權及結合性被解讀為
+
/ \
+ e3
/ \
e1 e2
所以我們知道 e3 跟 (e1+e2) 兩者的 value computation 要在最上面的 + 之前算完,
以及 e1 和 e2 的 value computation 要在 e1+e2 的 + 之前算完. 但是 e1, e2, e3
誰先算呢? 不知道.
此外我推文提到的 && || ?: 就是想要反駁說, expr1 && expr2 當中 && 的優先權
即使比 expr1, expr2 算式中的低, 但是 expr2 甚至根本不一定會計算到. 假使
expr1 為 false, (&&) 就計算為 false 了, 那計算順序不就比 expr2 更前面了?
同樣的 expr1 ? expr2 : expr3 這樣的 conditional, expr2 跟 expr3 也一定至
少有一個不會被算, 但 ? : 的結果還是會比不會算的那個先算出來.
※ 編輯: suhorng (36.225.42.192), 07/01/2015 01:21:08
推 LiloHuang: 我同意是因為 && 跟 || 有 short circuit evaluation 07/01 22:04
→ LiloHuang: 我一開始的說法並沒有考量到這些運算子,不夠嚴謹了 XD 07/01 22:04
→ LiloHuang: 但這也隱含著特定的判斷順序,機器碼仍是有規則的執行 07/01 22:09
因為語言是定義在 AST 上面的, 我會覺得, 既然 precedence 那些影響 parsing,
parsing 出來不同的 AST 又由定義有不同的結果, 當然要說 precedence 在 C++ 中
決定部份東西的先後順序是沒有問題...
不過真的只是部份順序喔. 像我舉的 e1 + e2 + e3 例子即使改成 e1 + e2 * e3,
仍然沒有說 e1, e2, e3 的 side-effects 甚至 value computation 誰先做誰後做.
我們是不知道的.
倒是有興趣的話, Haskell 是 non-strict 的, 通常都用 lazy evaluation 實現,
特點是一個東西真的被需要時才會被求值~ 例如
fib = 1 : 1 : zipWith (+) fib (tail fib)
當我們在 main 裡面 print (take 5 fib) 的時候, 會一個一個從頭開始去找 fib
的內容, 用到的才會去展開算出來, 這裡執行順序就更多的像是執行時才決定了.
※ 編輯: suhorng (36.225.42.192), 07/01/2015 23:01:08
※ 編輯: suhorng (36.225.42.192), 07/01/2015 23:01:43
推 LPH66: Lilo 你可以去看我貼的那篇, 那篇跟 && || 都無關 07/01 23:49
→ LPH66: 純粹是這個的 side-effect 跟那個的 value computation 間 07/01 23:49
→ LPH66: 誰先誰後的問題 07/01 23:49
→ LPH66: 而就是這個誰先誰後的不明確導致了那種寫法在不同編譯器 07/01 23:50
→ LPH66: 之間會得到不同的結果 07/01 23:50
推 LiloHuang: 我認同你的說法,你那篇的例子舉的相當恰當,謝啦 :) 07/02 00:03
→ LiloHuang: 某些有 side effect 情況下,順序的確跟編譯器實作相關 07/02 00:06
→ LiloHuang: 也許我太過在意的是 codegen 後的機器碼都有特定順序 07/02 00:14
→ suhorng: 偷渡 Haskell 失敗QQ (X 07/02 00:17
→ LiloHuang: 對 Haskell 不懂,用過 boost::phoenix 也能 lazy 求值 07/02 00:48
→ LiloHuang: 我知道你要講的意思啦 :) 07/02 00:48