Чому неможливо побудувати компілятор, який може визначити, чи змінює функція C ++ значення певної змінної?


104

Я читав цей рядок у книзі:

Неможливо побудувати компілятор, який насправді може визначити, змінить чи не функція C ++ значення певної змінної.

У параграфі йшлося про те, чому компілятор є консервативним під час перевірки на стабільність.

Чому неможливо побудувати такий компілятор?

Компілятор завжди може перевірити, чи змінена переназначена зміна, на неї викликається функція non-const або вона передається як параметр non-const ...


24
Перше, що мені спадає на думку - це динамічні бібліотеки зв’язків. Якщо я компілюю код на своїй машині, а ви компілюєте код на своїй машині, і ми пов'язуємо їх під час виконання , то як ваш компілятор міг знати, чи я змінив змінні чи ні?
Mooing Duck

4
@MooingDuck Саме так. У більш широкому розумінні, компілятор не компілює функцію окремо, але компілює її як частину більш широкої картини, яка може не входити до сфери компілятора.
закликав2voyage

3
"неможливе" може бути завищенням - "обчислювально нездійсненне" (як у NP-hard) може бути кращою характеристикою, але студенту це трохи важче зрозуміти. Уявіть пов'язаний список чи іншу абстрактну структуру даних. Якщо я закликаю функцію, яка змінює один вузол у цьому списку / дереві / що завгодно, як компілятор міг би сподіватися довести, який саме вузол було змінено (а може бути, що ще важливіше, який з них не було), не в основному повністю імітуючи програму з очікуваний вхід, але не потрібно 3 дні, щоб скласти один вихідний файл ...
twalberg

36
@twalberg Неможливе - це не завищення, проблема зупинки застосовується тут, як пояснюється декілька відповідей. Просто не можливо алгоритмічно повністю проаналізувати загальну програму.
Фіктик

5
@twalberg Компілятори, які складають лише підмножину дійсних програм, не дуже корисні.
Калеб

Відповіді:


139

Чому неможливо побудувати такий компілятор?

З тієї ж причини, що ви не можете написати програму, яка визначатиме, чи припиниться будь-яка програма. Це відома як проблема зупинки , і це одна з тих речей, які не піддаються обчисленню.

Щоб було зрозуміло, ви можете написати компілятор, який може визначити, що функція змінює змінну в деяких випадках , але ви не можете написати ту, яка надійно підкаже вам, що функція змінює (або зупинить) змінну (або зупинить) для всі можливі функції.

Ось простий приклад:

void foo() {
    if (bar() == 0) this->a = 1;
}

Як компілятор може визначити, чи лише fooколись перегляне цей код, чи зміниться a? Це чи ні, залежить від зовнішніх умов функції, а саме від виконання bar. Існує більше, ніж доказ того, що проблема зупинки не піддається обчисленню, але це вже добре пояснено у пов'язаній статті Вікіпедії (і в кожному підручнику з теорії обчислень), тому я не намагатимусь пояснити її правильно тут.


48
@mrsoltys, квантові комп'ютери "лише" експоненціально швидші для деяких проблем, вони не можуть вирішити невирішені проблеми.
zch

8
@mrsoltys Ці експоненціально складні алгоритми (як факторинг) ідеально підходять для квантових комп'ютерів, але проблема зупинки - це логічна дилема, вона не піддається обчисленню, незалежно від того, який тип комп'ютера у вас є.
користувач1032613

7
@mrsoltys, просто щоб бути розумним, так, це змінилося б. На жаль, це означало б, що алгоритм і припиняється, і все ще працює, на жаль, ви не можете сказати, що безпосередньо не спостерігаючи, яким чином ви впливаєте на фактичний стан.
Натан Ернст

9
@ ThorbjørnRavnAndersen: Гаразд, тому, припустимо, я виконую програму. Як саме я можу визначити, чи припиниться він?
ruakh

8
@ ThorbjørnRavnAndersen Але якщо ви дійсно виконуєте програму, і вона не закінчується (наприклад, нескінченний цикл), ви ніколи не дізнаєтесь, що вона не припиняється ... ви просто продовжуєте виконувати ще один крок, тому що це може бути останній ...
MaxAxeHax

124

Уявіть, такий компілятор існує. Припустимо також, що для зручності вона забезпечує функцію бібліотеки, яка повертає 1, якщо передана функція модифікує задану змінну, і 0, коли функція відсутня. Тоді що повинна друкувати ця програма?

int variable = 0;

void f() {
    if (modifies_variable(f, variable)) {
        /* do nothing */
    } else {
        /* modify variable */
        variable = 1;
    }
}

int main(int argc, char **argv) {
    if (modifies_variable(f, variable)) {
        printf("Modifies variable\n");
    } else {
        printf("Does not modify variable\n");
    }

    return 0;
}

12
Приємно! Я брехун парадокс , як написано програмістом.
Krumelur

28
Це насправді лише приємна адаптація відомого доказу нерозбірливості проблеми зупинки .
Костянтин Вайц

10
У цьому конкретному випадку "modifies_variable» має повернути true: Існує щонайменше один шлях виконання, у якому змінна дійсно модифікована. І цей шлях виконання досягається після виклику зовнішньої, недетермінованої функції - тому вся функція є недетермінованою. З цих двох причин компілятор повинен прийняти песимістичний погляд і вирішити, чи змінює змінну. Якщо шлях до зміни змінної досягнуто після детермінованого порівняння (перевіреного компілятором), дає помилковий (тобто "1 == 1"), тоді компілятор може сміливо сказати, що така функція ніколи не змінює змінну
Джо Пінеда

6
@JoePineda: Питання полягає в тому, чи fзмінює змінну - чи не може вона змінити змінну. Ця відповідь правильна.
Ніл Г

4
@JoePineda: ніщо не заважає мені скопіювати / вставити код modifies_variableз джерела компілятора, що повністю зводить нанівець ваш аргумент. (припустимо, що з відкритим кодом, але справа має бути зрозумілою)
orlp

60

Не плутайте "буде чи не буде змінювати змінну з урахуванням цих входів" для "має шлях виконання, який змінює змінну."

Перший називається непрозорим визначенням предикатів , і вирішити їх не можна - окрім скорочення проблеми, що зупиняється, ви можете просто вказати, що входи можуть надходити з невідомого джерела (наприклад, користувача). Це стосується всіх мов, а не лише C ++.

Останнє твердження, однак, можна визначити, подивившись на дерево розбору, що і роблять усі оптимізуючі компілятори. Причина, яку вони роблять, полягає в тому, що чисті функції референтно прозорі функції, для деякого визначення референтно прозорих ) мають всілякі приємні оптимізації, які можна застосувати, як, наприклад, їх легкодоступність або визначення їх значень під час компіляції; але щоб знати, чи функція чиста, ми повинні знати, чи може вона коли-небудь змінювати змінну.

Отже, те, що здається дивним твердженням про C ++, насправді є тривіальним твердженням про всі мови.


5
Це найкраща відповідь, що важливо зробити це.
UncleZeiv

"тривіально неможливо"?
Кіп

2
@Kip "тривіально неможливо вирішити", ймовірно, означає "неможливо вирішити, а доказ тривіальний".
fredoverflow

28

Я думаю, що ключове слово "чи змінить функцію C ++ значення певної змінної" чи "буде" буде ". Безумовно, можливо створити компілятор, який перевіряє, чи дозволяється функція C ++ змінювати значення певної змінної, ви не можете з упевненістю сказати, що зміни відбудуться:

void maybe(int& val) {
    cout << "Should I change value? [Y/N] >";
    string reply;
    cin >> reply;
    if (reply == "Y") {
        val = 42;
    }
}

"Звичайно, можна створити компілятор, який перевіряє, чи може функція C ++ змінити значення певної змінної. Ні, це не так. Дивіться відповідь Калеба. Щоб компілятор знав, чи може foo () змінити a, він повинен знати, чи можна bar () повернути 0. І немає обчислювальної функції, яка могла б повідомити всі можливі значення повернення будь-якої обчислюваної функції. Отже, існують такі кодові шляхи, що компілятор не зможе визначити, чи вони коли-небудь будуть досягнуті. Якщо змінна буде змінена лише в
кодовому

12
@MartinEpsz Під "може" я маю на увазі "дозволено змінювати", а не "може змінитися". Я вважаю, що саме це було на увазі ОП, коли говорили про constперевірку стану.
dasblinkenlight

@dasblinkenlight Я повинен був би погодитися, що я вважаю, що ОП, можливо, означала перше, "дозволено змінити", або "може або не може змінитися" проти "точно не зміниться". Звичайно, я не можу придумати сценарій, коли це було б проблемою. Ви навіть можете змінити компілятор, щоб просто відповісти "може змінитися" на будь-яку функцію, що містить або ідентифікатор, або виклик функції, яка має атрибут відповіді "може змінитися". Однак, C і C ++ - це жахливі мови, щоб спробувати це, оскільки вони мають таке нещільне визначення речей. Я думаю, що саме тому цінність взагалі буде проблемою в C ++.
DDS

@MartinEpsz: "І немає обчислювальної функції, яка могла б повідомити всі можливі значення повернення будь-якої обчислюваної функції". Я думаю, що перевірка "всіх можливих повернених значень" - це неправильний підхід. Існують математичні системи (максимуми, математика), які можуть вирішувати рівняння, а це означає, що було б доцільно застосувати аналогічний підхід до функцій. Тобто трактувати це як рівняння з кількома невідомими. Проблеми полягають у контролі потоку + побічні ефекти => нерозв'язні ситуації. IMO, без цих (функціональна мова, без призначення / побічних ефектів), можна було б передбачити, який шлях буде проходити програма
SigTerm

16

Я не думаю, що потрібно викликати проблему зупинки, щоб пояснити, що ви не можете алгоритмічно знати під час компіляції, чи буде дана функція змінювати певну змінну чи ні.

Натомість досить зазначити, що поведінка функції часто залежить від умов виконання, про які компілятор не може знати заздалегідь. Напр

int y;

int main(int argc, char *argv[]) {
   if (argc > 2) y++;
}

Як компілятор міг із впевненістю передбачити, чи yбуде модифікований?


7

Це можна зробити, і компілятори роблять це постійно для деяких функцій , це, наприклад, тривіальна оптимізація для простих вбудованих аксесуарів або багатьох чистих функцій.

Що неможливо - це знати в загальному випадку.

Кожного разу, коли є системний виклик або виклик функції, що надходить з іншого модуля, або виклик методу, який може бути перекрито, може статися все, що завгодно, включаючи вороже захоплення від використання деяким хакером переповнення стека для зміни непов'язаної змінної.

Однак вам слід використовувати const, уникати глобалів, надавати перевагу посиланням на покажчики, уникати повторного використання змінних для непов'язаних завдань тощо, що полегшить життя компілятора при здійсненні агресивних оптимізацій.


1
Якщо я правильно це пригадую, у цьому і полягає вся суть функціонального програмування, правда? Використовуючи лише суто детерміновані функції без побічних ефектів, компілятори можуть робити агресивну оптимізацію, попереднє виконання, після виконання, запам'ятовування та навіть виконання під час компіляції. Справа в тому, що я думаю, що багато хто з ігнорують відповідають (або плутати о) є те , що це дійсно можливо для добре себе підмножина всіх програм . І ні, ця підмножина не є тривіальною чи нецікавою, насправді вона дуже корисна. Але це абсолютно неможливо для абсолютного загального випадку.
Джо Пінеда

Перевантаження - це концепція часу компіляції. Ви, мабуть, мали на увазі "переотриманий метод".
fredoverflow

@FredOverflow: так, я маю на увазі переосмислення. Перевантаження - це справді концепція часу компіляції. Дякуємо, що помітили його (звичайно, якщо реалізація надходить з іншого підрозділу компіляції, у компілятора все ще можуть виникнути проблеми з його аналізом, але я це не мав на увазі). Я виправлю відповідь.
kriss

6

Для пояснення цього існує кілька способів, одним з яких є проблема зупинки :

У теорії обчислюваності проблема зупинки може бути викладена так: "Давши опис довільної комп'ютерної програми, вирішіть, чи закінчується програма запущена чи продовжує працювати вічно". Це еквівалентно проблемі прийняття рішення щодо даної програми та вводу, чи програма врешті зупиниться під час запуску із цим входом, чи запуститься назавжди.

Алан Тьюрінг довів у 1936 році, що загальний алгоритм вирішення проблеми зупинки для всіх можливих пар програм-введення не може існувати.

Якщо я напишу програму, яка виглядає приблизно так:

do tons of complex stuff
if (condition on result of complex stuff)
{
    change value of x
}
else
{
    do not change value of x
}

Чи xзмінюється значення ? Щоб визначити це, спершу слід було б визначити, чи do tons of complex stuffвикликає деталь стан вогню - чи ще більш базовий, чи зупиняється він. Це те, що компілятор не може зробити.


6

Дуже здивований, що немає відповіді, що безпосередньо використовувати проблему зупинки! Існує дуже очевидне скорочення від цієї проблеми до проблеми зупинки.

Уявіть, що компілятор міг би визначити, змінила чи не функція значення змінної. Тоді, безумовно, можна було б сказати, чи змінюється наступна функція значення y чи ні, припускаючи, що значення x можна відстежувати у всіх викликах протягом усієї іншої програми:

foo(int x){
   if(x)
       y=1;
}

Тепер для будь-якої програми, яка нам подобається, давайте перепишемо її як:

int y;
main(){
    int x;
    ...
    run the program normally
    ...
    foo(x);
}

Зауважте, що, якщо і лише якщо наша програма змінює значення y, чи вона тоді припиняється - foo () - це останнє, що вона робить перед виходом. Це означає, що ми вирішили проблему зупинки!

Наведене вище зменшення показує нам, що проблема визначення того, чи змінюється значення змінної, принаймні настільки ж важка, як і проблема зупинки. Проблема зупинки, як відомо, є непорушною, тому ця також повинна бути.


Я не впевнений, що я дотримуюся ваших міркувань щодо того, чому наша програма припиняється, якщо вона змінює значення y. Мені здається, що foo()швидко повертається, а потім main()виходить. (Крім того, ви телефонуєте foo()без аргументів ... це частина моєї плутанини.)
LarsH

1
@LarsH: Якщо змінена програма припиняється, остання функція, яку вона викликала, була f. Якщо y було змінено, виклик f (інші заяви не можуть змінити y, оскільки він був введений лише модифікацією). Отже, якщо y було змінено, програма припиняється.
MSalters

4

Як тільки функція викликає іншу функцію, про яку компілятор не "бачить" джерело, вона або повинна припустити, що змінна змінена, або все може піти не так далі нижче. Наприклад, скажімо, у нас це є у "foo.cpp":

 void foo(int& x)
 {
    ifstream f("f.dat", ifstream::binary);
    f.read((char *)&x, sizeof(x));
 }

і у нас це є у "bar.cpp":

void bar(int& x)
{
  foo(x);
}

Як компілятор може «знати», що xне змінюється (або змінюється, адекватніше) bar?

Я впевнений, що ми можемо придумати щось складніше, якщо це не досить складно.


Компілятор може знати, що x не змінюється в рядку, якщо рядок x передається як передача посилань на const, правда?
Крикетер

Так, але якщо я додаю const_castfoo, він все одно xзміниться - я б порушив договір, який говорить про те, що ви не повинні змінювати constзмінні, але оскільки ви можете перетворити що завгодно на "більш const", і const_castіснує, дизайнери мови, безумовно, мали на увазі, що іноді є вагомі причини вважати, що constзначення можуть потребувати змін.
Матс Петерсон

@MatsPetersson: Я вважаю, що якщо ви const_cast, ви зможете зберегти всі фрагменти, які зламаються, тому що компілятор може, але не повинен це компенсувати.
Zan Lynx

@ZanLynx: Так, я впевнений, що це правильно. Але в той же час акторський склад існує, а це означає, що хтось, хто розробив мову, мав якусь думку про те, що "нам це може знадобитися в якийсь момент" - це означає, що це не означає, щоб взагалі нічого корисного не робити.
Матс Петерсон

1

Загалом неможливо, щоб компілятор визначив, чи буде змінена змінна , як було зазначено.

Під час перевірки стійкості питання, як видається , викликає інтерес, чи може змінна може бути змінена функцією. Навіть це важко в мовах, які підтримують покажчики. Ви не можете керувати тим, що інший код робить із вказівником, він навіть може бути прочитаний із зовнішнього джерела (хоча малоймовірно). У мовах, що обмежують доступ до пам’яті, такі види гарантій можуть бути можливими і дозволяють зробити більш агресивну оптимізацію, ніж це робить C ++.


2
Одне, що я хотів би підтримати в мовах, - це відмінність між ефемерними, поверненими та стійкими посиланнями (або покажчиками). Ефемерні посилання можуть бути скопійовані лише до інших ефемерних посилань, зворотні - можуть бути скопійовані в ефемерні або зворотні, а стійкі - можна скопіювати будь-яким способом. Повернене значення функції буде обмежено самим обмежуючим аргументом, який передається як "повертаються" параметри. Я вважаю прикро, що в багатьох мовах, коли проходить посилання, нічого не вказується, як довго він може використовуватися.
суперкар

Це, безумовно, було б корисно. Звичайно, для цього є зразки, але в C ++ (і багатьох інших мовах) завжди можна «обдурити».
Крумельюр

Основним способом, у якому .NET перевершує Java, - це те, що він має концепцію ефемерного посилання, але, на жаль, немає можливості об'єкти виставити властивості як ефемерні посилання (те, що я дуже хотів би бачити, було б засобом який код із використанням властивості передав би ефемерну посилання на код (разом з тимчасовими змінними), який слід використовувати для маніпулювання об'єктом.
supercat

1

Щоб зробити питання більш конкретним, я пропоную наступний набір обмежень, який, можливо, мав на увазі автор книги:

  1. Припустимо, що компілятор вивчає поведінку певної функції стосовно стійкості змінної. Для коректності компілятору доведеться припустити (через псевдонім, як пояснено нижче), якщо функція, яка називається іншою функцією, змінна змінена, тому припущення №1 застосовується лише до фрагментів коду, які не здійснюють виклики функції.
  2. Припустимо, що змінна не змінюється асинхронною чи одночасною активністю.
  3. Припустимо, компілятор визначає лише те, чи змінна може бути змінена, а не чи буде вона змінена. Іншими словами, компілятор виконує лише статичний аналіз.
  4. Припустимо, компілятор розглядає лише правильно функціонуючий код (не враховуючи перевитрат / підробок масиву, погані покажчики тощо)

В контексті дизайну компілятора, я думаю, припущення 1,3,4 мають ідеальний сенс у погляді письменника-компілятора в контексті правильності ген коду та / або оптимізації коду. Припущення 2 має сенс у відсутності мінливого ключового слова. І ці припущення також зосереджують питання достатньо, щоб зробити судження про запропоновану відповідь набагато більш виразним :-)

З огляду на ці припущення, основна причина, чому не можна припустити стабільність, пов'язана із змінним псевдонімом. Компілятор не може знати, чи інша змінна вказує на змінну const. Згладжування може бути пов’язане з іншою функцією в тому ж блоці компіляції; у цьому випадку компілятор може переглядати всі функції та використовувати дерево викликів, щоб статично визначити, що може відбуватися випробовування. Але якщо псевдонім пов'язаний з бібліотекою чи іншим іноземним кодом, тоді у компілятора немає можливості дізнатися при введенні функції, чи є змінні псевдонімом.

Ви можете стверджувати, що якщо змінна / аргумент позначена const, то вона не повинна підлягати зміні через псевдонім, але для письменника-компілятора це досить ризиковано. Для людського програміста навіть може бути ризиком оголосити const змінної як частину, скажімо, великого проекту, де він не знає поведінки всієї системи, або ОС, або бібліотеки, щоб дійсно знати, що змінена перемогла ' t змінити.


0

Навіть якщо оголошена змінна const, це не означає, що якийсь неправильно написаний код може її замінити.

//   g++ -o foo foo.cc

#include <iostream>
void const_func(const int&a, int* b)
{
   b[0] = 2;
   b[1] = 2;
}

int main() {
   int a = 1;
   int b = 3;

   std::cout << a << std::endl;
   const_func(a,&b);
   std::cout << a << std::endl;
}

вихід:

1
2

Це трапляється тому, що aі bє змінними стека, і b[1]просто відбувається таке ж місце, як і в пам'яті a.
Марк Лаката

1
-1. Невизначена поведінка знімає всі обмеження щодо поведінки компілятора.
MSalters

Невпевнені в голосуванні проти. Це лише приклад, який стосується оригінального питання ОП про те, чому компілятор не може з'ясувати, чи є щось справді, constякщо все позначено міткою const. Це тому, що невизначена поведінка є частиною C / C ++. Я намагався знайти інший спосіб відповісти на його запитання, а не згадувати проблему зупинки чи зовнішній внесок людини.
Марк Лаката

0

Для розширення моїх коментарів, текст цієї книги незрозумілий, що заплутує проблему.

Як я коментував, ця книга намагається сказати: "давайте отримаємо нескінченну кількість мавп, щоб написати кожну можливу функцію C ++, яку можна було б коли-небудь записати. Будуть випадки, коли ми виберемо цю змінну (якусь певну функцію написали мавпи) використовує, ми не можемо визначити, чи змінить функцію цю змінну. "

Звичайно, для деяких (навіть багатьох) функцій у будь-якій програмі це може бути визначено компілятором, і це дуже легко. Але не для всіх (або обов'язково більшості).

Цю функцію можна легко проаналізувати:

static int global;

void foo()
{
}

"foo" явно не змінює "глобальний". Він взагалі нічого не модифікує, і компілятор може це зробити дуже легко.

Цю функцію неможливо так проаналізувати:

static int global;

int foo()
{
    if ((rand() % 100) > 50)
    {
        global = 1;
    }
    return 1;

Оскільки дії "foo" залежать від значення, яке може змінюватися під час виконання , воно очевидно не може бути визначене під час компіляції чи буде воно модифікувати "глобальне".

Всю цю концепцію зрозуміти набагато простіше, ніж комп'ютерні вчені. Якщо функція може робити щось інше на основі того, що речі можуть змінюватися під час виконання, тоді ви не можете розробити те, що вона буде робити, доки вона не запуститься, і кожен раз, коли вона запускається, вона може робити щось інше. Незалежно від того, неможливо це чи ні, очевидно неможливо.


те, що ви говорите, є правдою, але навіть для дуже простих програм, про які все відомо під час компіляції, ви нічого не зможете довести, навіть якщо програма зупиниться. Це проблема зупинки. Наприклад, ви можете написати програму на базі Hailstone Sequences en.wikipedia.org/wiki/Collatz_conjecture і змусити її повернути правду, якщо вона сходиться до однієї. Компілятори не зможуть це зробити (оскільки це переповнюватиметься у багатьох випадках), і навіть математики не знають, правда це чи ні.
kriss

Якщо ви маєте на увазі "є кілька дуже простих програм, для яких ви нічого не можете довести", я цілком згоден. Але класичне підтвердження проблеми зупинки Тьюрінга по суті покладається на те, що сама програма може сказати, чи зупиняється вона, щоб встановити протиріччя. Оскільки це математика не реалізація. Звичайно, є програми, які цілком можливо статично визначити під час компіляції, чи буде змінена певна змінна, і чи зупинятиметься програма. Це може бути математично недоступно, але практично досяжне у певних випадках.
El Zorko
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.