понедельник, августа 22, 2005

Приведение типов в C++

Лучшая практика по приведению типов: не делать этого. Потому что, если в программе потребовалось приведение типов, значит в этой программе с большой долей вероятности что-то неладно. Для довольно редких ситуаций, когда это все-таки действительно нужно, есть четыре способа приведения типов. Старый, оставшийся со времен C, но все еще работающий, лучше не использовать вовсе. Хотя бы потому, что конструкцию вида (Тип) очень сложно обнаружить при чтении кода программы.

const_cast
Самое простое приведение типов. Убирает так называемые cv спецификаторы (cv qualifiers), то есть const и volatile. volatile встречается не очень часто, так что более известно как приведение типов, предназначенное для убирания const. Если приведение типов не удалось, выдается ошибка на этапе компиляции.
При использовании остальных приведений типов cv спецификаторы останутся как были.

const char *str = "hello";
char *str1 = const_cast<char*>(str);

Updated 20.07.2008
Пример несколько неудачен. Если снимать const с переменной, которая изначально была const, то дальнейшее её использование приведёт к undefined behaviour. Вот хороший пример:
int i;
const int * pi = &i;
// *pi имеет тип const int,
// но pi указывает на int, который константным не является
int* j = const_cast<int *> (pi);


static_cast
Может быть использован для приведения одного типа к другому. Если это встроенные типы, то будут использованы встроенные в C++ правила их приведения. Если это типы, определенные программистом, то будут использованы правила приведения, определенные программистом.
static_cast между указателями корректно, только если один из указателей - это указатель на void или если это приведение между объектами классов, где один класс является наследником другого. То есть для приведения к какому-либо типу от void*, который возвращает malloc, следует использовать static_cast.
int * p = static_cast<int*>(malloc(100));
Если приведение не удалось, возникнет ошибка на этапе компиляции. Однако, если это приведение между указателями на объекты классов вниз по иерархии и оно не удалось, результат операции undefined. То есть, возможно такое приведение: static_cast<Derived*>(pBase), даже если pBase не указывает на Derived, но программа при этом будет вести себя странно.

dynamic_cast
Безопасное приведение по иерархии наследования, в том числе и для виртуального наследования.
dynamic_cast<derv_class *>(base_class_ptr_expr)
Используется RTTI (Runtime Type Information), чтобы привести один указатель на объект класса к другому указателю на объект класса. Классы должны быть полиморфными, то есть в базовом классе должна быть хотя бы одна виртуальная функция. Если эти условие не соблюдено, ошибка возникнет на этапе компиляции. Если приведение невозможно, то об этом станет ясно только на этапе выполнения программы и будет возвращен NULL.
dynamic_cast<derv_class &>(base_class_ref_expr)
Работа со ссылками происходит почти как с указателями, но в случае ошибки во время исполнения будет выброшено исключение bad_cast.

reinterpret_cast
Самое нахальное приведение типов. Не портируемо, результат может быть некорректным, никаких проверок не делается. Считается, что вы лучше компилятора знаете как на самом деле обстоят дела, а он тихо подчиняется. Не может быть приведено одно значение к другому значению. Обычно используется, чтобы привести указатель к указателю, указатель к целому, целое к указателю. Умеет также работать со ссылками.

reinterpret_cast<whatever *>(some *)
reinterpret_cast<integer_expression>(some *)
reinterpret_cast<whatever *>(integer_expression)

Чтобы использовать reinterpret_cast нужны очень и очень веские причины. Используется, например, при приведении указателей на функции.


Что делает приведение типов в стиле С: пытается использовать static_cast, если не получается, использует reinterpret_cast. Далее, если нужно, использует const_cast .

Примеры
unsigned* и int* никак не связаны между собой. Есть правило приведения между unsigned (int) и int, но не между указателями на них. И привести их с помощью static_cast не получится, придется использовать reinterpret_cast. То есть вот так работать не будет:
unsigned* v_ptr;
cout << *static_cast<int*>(v_ptr) <<endl;


Приведение вниз по иерархии:
class Base { public: virtual ~Base(void) { } };
class Derived1 : public Base { };
class Derived2 : public Base { };
class Unrelated { };

Base* pD1 = new Derived1;
Вот такое приведение корректно: dynamic_cast<Derived1 *>(pD1);
А вот такое возвратит NULL: dynamic_cast<Derived2 *>(pD1);
Никак не связанные указатели можно приводить с помощью reinterpret_cast:
Derived1  derived1;
Unrelated* pUnrelated = reinterpret_cast<Unrelated*>(&derived1);

Пример использования static_cast:
int*   pi;
void* vp = pi;
char* pch = static_cast<char*>(vp);

Примеры использования reinterpret_cast:
float f (float);
struct S {
float x;
float f (float);
} s;
void g () {
reinterpret_cast<int *>(&s.x);
reinterpret_cast<void (*) ()>(&f);
reinterpret_cast<int S::*>(&S::x);
reinterpret_cast<void (S::*) ()>(&S::f);
reinterpret_cast<void**>(reinterpret_cast<long>(f));
}

Приведение в стиле C можно использовать, чтобы избавиться от значения, возвращаемого функцией. Польза от этого сомнительная, правда...
string sHello("Hello");
(void)sHello.size(); // Throw away function return
Также я видела использование приведение типов в стиле С для приведения к приватному базовому классу, но для этого можно использовать и reinterpret_cast.

Ссылки по теме:
1996 С++ Drawft Standarts - Expressions
compl.lang.c++.moderated. static_cast vs. reinterpret_cast
borland.public.cppbuilder.vcl.components.using. static_cast, dynamic_cast, reinterpret_cast
compl.lang.c++.moderated. static cast
compl.lang.c++.moderated. New-Style Casts

Technorati tag:

54 коммент.:

Анонимный комментирует...

А зачем Вы пишите это в своём блоге? Памятка для самого себя или для других.

PS: Я читаю. Очень классно. Получше чем у нашего преподавателя…

Alena комментирует...

Поскольку этот вопрос мне задается уже не в первый раз, ответила отдельным постом.

Анонимный комментирует...

Спасибо.

Анонимный комментирует...

Вроде всё написано, но вопросов больше чем ответов. К тому же встречаются неточности.

const char *str = "hello";
char *str1 = const_cast<char>(str);

Для начала надо бы

const char * str = "hello";
char * str1 = const_cast<char *>(str);

Аналогично забыты звёздочки ещё в куче мест.

Кроме того так делать категорически нельзя. Память под строковые константы будет скорее всего доступна только на чтение и при попытке записи вылетит очень некрасивое исключение.

После фразы "Умеет также работать со ссылками" видим работу с указателями :)

reinterpret_cast<whatever>(some *)

reinterpret_cast<integer_expression>(some *)

Итого: За идёю 5, за реализацию 3.

Alena комментирует...

Аналогично забыты звёздочки ещё в куче мест.
И не только звездочки. Вернула все на место.

Alena комментирует...

Кроме того так делать категорически нельзя. Память под строковые константы будет скорее всего доступна только на чтение и при попытке записи вылетит очень некрасивое исключение.
Даже если работать не со строкой, запись в реально константный объект даст undefined behavior, насколько я помню. Но можно снять const, чтобы передать переменную в функцию из не const-корректной библиотеки. При условии, что точно знаешь, что внутри этой самой функции писать никто ничего не будет. Это не самое хорошее решение, но в принципе так сделать можно.

Анонимный комментирует...

Тут какое дело. То что Undefined будет в любом случае это ясно.

Но если в случае объектов этот номер ещё может пройти, то в случае константных строк они могут (зависит от компилятора и процессора) располагаться в сегменте памяти защищённом от записи на аппаратном уровне.

Кстати, раз уж затронули константность, неплохо бы про mutable написать :)

Alena комментирует...

Кстати, раз уж затронули константность, неплохо бы про mutable написать :)

Дык уже: mutable и const_cast

Unknown комментирует...

Есть ещё интересное свойство reinterpret_cast, которое осталось в тени. Нельзя приводить указатель на функцию к указателю на объект:

int foo ();
reinterpret_cast < void* > (foo); //ошибка

По поводу преобразования к (void) - это единственно C-style преобразование, аналога которому нет в C++. Эту операцию можно применять для борьбы с предупреждением о неиспользуемой переменной:

void foo (int i) {
assert (i > 2);
(void) i; // без этой строчки будет warning в оптимизированной версии
}

Анонимный комментирует...

Боже, это прекрасно, что вы пишете про ЭТО. Конкретно у Вас узнал про "reinterpret_cast" - страшная штука.)

Анонимный комментирует...

int var = 7;

const int * p3 = new int ( var );
*const_cast < int * > ( p3 ) = 3;
std::cout << *p3 << std::endl;
delete p3; p3 = NULL;

Этот код выполняется нормально. Освобождаем память, присваиваем указателю NULL.

А как сделать к примеру с константой-указателем?

int * const p4 = new int ( var );
delete p4;
const_cast < int* > ( p4 ) = NULL; // <---- Error: lvalue required as left operand of assignment

Alena комментирует...

2Анонимный:
А как сделать к примеру с константой-указателем?

Судя по вопросу, ты скомпилял эти два примера одним компилятором, после чего сделал вывод, что есть разница в приведении типов между const int * p3 и int * const p4.
На самом деле ситуация такая. Результат const_cast для указателей не является lvalue и не может стоять слева в операции присваивания (см. Стандарт 5.2.11 ). Некоторые компиляторы в некоторых случаях это пропускают, но они не правы. Твой компилятор совершенно справедливо ругается на const_cast < int* > ( p4 ) = NULL.
Можно сделать так:
int * p = const_cast < int* > ( p4 ); p = NULL;

Можно сделать так:
const_cast < int*& > ( p4 ) = NULL;

Да, поскольку все переменные изначально определены как const, то результат всех этих действий - undefined behavior. Поскольку ты убираешь const с объекта, который изначально являлся константным, а потом делаешь ему присваивание.

Анонимный комментирует...

2Алёна:

_Спасибо_, работает.
Начал эту затею чтобы случайно не переписовать адреса указателей в циклах и предотвращать утечку памяти...
Вишло как всегда ))

Анонимный комментирует...

А мы вышему блогу к экзамену готовимся :)
Спасибо

Анонимный комментирует...

Cпасибо класный блог, а есть вариант по STL(Контейнерам)

Alena комментирует...

2Анонимный:
Cпасибо класный блог, а есть вариант по STL(Контейнерам)

Ну про вариант не скажу, но кое-что про контейнеры я писала. Поройтесь в постах с тегом cpp.

Анонимный комментирует...

Спасибо большое, некоторые моменты стали намного понятнее.
Почитал комментарии, и понял, что надо ещё во многом разбираться. За то тоже спасибо =)

Анонимный комментирует...

А вот такой вопрос! Как самому определить правило для приведения объектов классов?

Alena комментирует...

2Анонимный:
А вот такой вопрос! Как самому определить правило для приведения объектов классов

Я так поняла, нужно вот это: Overloading typecasts

Анонимный комментирует...

Здравствуйте
Повторю за ост участниками спасибо
Вы начали статью с того, что приведения типов впринципе надо избегать. А как вывести std::string в консоль?

Alena комментирует...

>А как вывести std::string в консоль?

std::string MyString;

...

cout << MyString;

Анонимный комментирует...

binary '<<' : no operator found which takes a right-hand operand of type 'std::string' (or there is no acceptable conversion)

Возможно, cout << MyString.data();
К простому char* std::string не приводится

Alena комментирует...

2AP:

binary '<<' : no operator found which takes a right-hand operand of type 'std::string' (or there is no acceptable conversion)

Эммм.. #include <iostream> есть? Судя по сообщению об ошибке, << трактуется как бинарный сдвиг.

Я код проверила, прежде чем сюда постить. :-)

К простому char* std::string не приводится

Приводится к const char*, MyString.c_str(). Но оно тут не нужно.

Анонимный комментирует...

Alena, спасибо вам огромное!

Tele комментирует...

Также я видела использование приведение типов в стиле С для приведения к приватному базовому классу, но для этого можно использовать и reinterpret_cast.

Важный момент: использовать reinterpret_cast можно, если приводить к базовому классу, который во всей иерархии наследования наследуется первым.

Т.е. преобразование указателя на наследуемый класс к указателю на базовый класс, который наследуется приватно вторым, можно сделать только с использованием C-style cast.

Можно еще попробовать подключить арифметику указателей и надеется, что компилятор расположит объекты базовых классов в соответствии с их размерами. А если и расположит, то код станет непереносимым. Кроме того, для каждого подобного преобразования придется писать свой код.

Пример:

class base1{
int a;
};

class base2{
int a;
};

class derived: public base1, base2{};

int main(){
derived *d=new derived;
base2 *b1=(base2*) d;
base2 *b2=reinterpret_cast (d);
cout<<d<<endl; //001D4130
cout<<b1<<endl;//001D4134
cout<<b2<<endl;//001D4130
cin.ignore();
}

Unknown комментирует...

GCC (точнее MinGW) позволяет вот такую штуку, A и B — не связанные друг с другом классы:

A* a = new A();
void* empty = (void*)a;
B* = dynamic_cast<A*>((A*)empty);

Валера комментирует...

прошу прощения скобки порезались в прошлый раз

Для преобразования указателей лучше использовать следующую конструкцию:

class A{};
A a;
char* c = pointer_cast<char*>(&a);
A* pa = pointer_cast<A*>(c);

это позволит избежать проблемы преобразования числа в указатель с помощью reinterpret_cast

это вызовет ошибку компиляции
int i = 10;
A *a = pointer_cast<A*>(i);

а это нет
int i = 10;
A *a = reinterpret_cast<A*>(i);


реализация pointer_cast должна быть такая, и она не имеет отношения к boost, просто совпадают имена

namecpace tools
{

template<typename result, typename source>
result pointer_cast(source *v)
{
return static_cast<result>(static_cast<void*>(v));
}

template<typename result, typename source>
result pointer_cast(const source *v)
{
return static_cast<result>(static_cast<const void*>(v));
}

}

это просто двойной static_cast через указатель на void

Alena комментирует...

2Валера:

Для преобразования указателей лучше использовать следующую конструкцию:

Тут стоит уточнить, что pointer_cast входит в boost, т.е. это приведение типов не из Стандарта.

Анонимный комментирует...

"Лучшая практика по приведению типов: не делать этого. Потому что, если в программе потребовалось приведение типов, значит в этой программе с большой долей вероятности что-то неладно"

Это потому, что с большой долей выероятности вы пишите исключительно высокоуровневые приложения.

"Хотя бы потому, что конструкцию вида (Тип) очень сложно обнаружить при чтении кода программы."

Это видимо Вам сложно...

Подобные введения очень хорошо говорят о Вашем прифессиональном уровне.

Анонимный комментирует...

Если есть класс A и класс B, который наследуется от A, то что правильнее использовать, чтобы привести B к A? Вроде и static_cast и dynamic_cast работают корректно.

Alena комментирует...

Анонимный
Если есть класс A и класс B, который наследуется от A, то что правильнее использовать, чтобы привести B к A? Вроде и static_cast и dynamic_cast работают корректно.

Для приведения по иерархии лучше использовать dynamic_cast. Потому что, если вы ошибетесь и начнете приводить B, который на самом деле не является наследником A, то с dynamic_cast это будет поймать легче.

Анонимный комментирует...

если вы ошибетесь и начнете приводить B, который на самом деле не является наследником A, то с dynamic_cast это будет поймать легче

dynamic_cast не может привести B к А, только B* к A*, причем отсутствие наследования он поймает только во время выполнения, вернув NULL, и это нужно предусмотреть в коде. А static_cast в обоих случаях ругнется уже при компиляции.

И вообще стоит подумать, нужно ли приводить производный класс к базовому явно, когда это может делаться неявно.

Анонимный комментирует...

...А, да. Еще dynamic_cast может B& привести к A&, тогда при несовместимости он возбуждает.

Анонимный комментирует...

По сравнению с Delphi - какой-то маразм и извращение!!!

Алексей комментирует...

Где-то это я уже видел...
Ваш пост популярен!

Анонимный комментирует...

Помогите, в чём ошибка:

void addem(void *invec, void *inoutvec, int *len, MPI_Datatype *dtype)
{
int i;
for(i=0;i<*len;i++)
{
*reinterpret_cast(inoutvec) = reinterpret_cast(inoutvec) + reinterpret_cast(invec)*(i+1);
invec = reinterpret_cast(invec)+sizeof(int);
inoutvec = reinterpret_cast(inoutvec)+sizeof(int);
}
}

Вот такая ошибка: error C2296: *: недопустимо, левый операнд имеет тип "int *"

Alena комментирует...

Анонимный

Помогите, в чём ошибка:

void addem(void *invec, void *inoutvec, int *len, MPI_Datatype *dtype)
{


Blogger, к сожалению, удалил все, что было между угловыми скобками, так что непонятно к чему именно приводятся значения. Если еще не нашли ответ, замените, плз, угловые скобки на < и > или напишите мне по почте. Можно еще на rsdn.ru поспрашивать.

Анонимный комментирует...

> *reinterpret_cast(inoutvec) = reinterpret_cast(inoutvec) + reinterpret_cast(invec)*(i+1);

Как аноним анониму: справа разыменовать, похоже, забыли, и множат-складывают пойнтеры.

Lavir the Whiolet комментирует...

Вопрос: кастов так много, потому что все они реализуют фундаментально разные концепции, или потому что Страуструп не осилил свести их к одному-единственному cast?

Анонимный комментирует...

Божественный блог, божественная статья. Спасибо, автор!

Unknown комментирует...

Алёна! Ты лучший девушка-програмист С++ !

Unknown комментирует...

Потому что, если в программе потребовалось приведение типов, значит в этой программе с большой долей вероятности что-то неладно.

А как на счет программирования на WinApi, где все функции с которыми работаешь принимают или LPARAM или WPARAM и что либо передать без приведения типа просто не выйдет???

Alena комментирует...

MrAndreykka

Это один из тех случаев, где приведение типов имеет смысл.

Дима комментирует...

>>*pi имеет тип const int, но pi указывает на int, который константным не является что
По-моему, очень кривая формулировка, сбивающая с толку :(
Ведь const здесь вообще не причем, конструкция const type* лишь означает, что тип указателя нельзая изменить (и то я не уверен насчет как раз таки кастования, сработает оно с такими константными указателями, или нет)

Unknown комментирует...

Девять лет продолжается бой.
И конца пока не видно.

glueckware комментирует...

Имхо лучше написать: dynamic_cast нужен для приведения вниз по иерархиия наследования.
Потому что заранее неизвестно, на какой конкретный объект указывает приводимый указатель.
По сути, dynamic_cast - это проверка на принадлежность к конкретному классу ниже по иерархии.

//Грубо говоря - является ли данной абстрактное животное кошкой? если да, ты мяукнуть:

Animal *anAnimal = new Cat("Мурка");

/// ...


try {
Cat *cat = dynamic_cast(anAnimal);
cat->miew();
}
catch (bad_cast) {
// No miew
}

Прямым аналогом из Java, например, является операция instanceof с последующим явным приведением.
В Delphi - is и as.

Однако, Мейерс указывает, что следует избегать dynamic_cast и стараться вместо него использовать полиморфизм - это вроде как более прямо и позволяет избежать включения RTTI.
В нашем случае это выглядело бы примерно так:
class Animal {
public:
virtual void say() = 0;
// ...
};

class Cat: public Animal {
public:
virtual void say() { miew(); }
private:
void miew();
};

//Тогда вызываеющий код будет без преобразований:

anAnimal->say();

В результате кошка мяукнет, а собака залает без всяких *_cast.

};

glueckware комментирует...

Сорри, ошибся два раза (давно на С++ не писал)
вместо:

try {
Cat *cat = dynamic_cast(anAnimal);
cat->miew();
}
catch (bad_cast) {
// No miew
}

следует читать:
Cat *cat;
if ((cat = dynamic_cast(anAnimal)) != NULL) {
cat->miew();
}

Unknown комментирует...


Используется RTTI (Runtime Type Information), чтобы привести один указатель на объект класса к другому указателю на объект класса. Классы должны быть полиморфными, то есть в базовом классе должна быть хотя бы одна виртуальная функция. Если эти условие не соблюдено, ошибка возникнет на этапе компиляции.


и


Если есть класс A и класс B, который наследуется от A, то что правильнее использовать, чтобы привести B к A? Вроде и static_cast и dynamic_cast работают корректно.


Ошибкой компиляции dynamic_cast будет неудачная попытка приведения вниз (от базового к наследнику) по не-полиморфной иерархии классов: даже если приведение производится корректно.
Если приводить вверх по иерархии классов то всё работает ок.

RTTI для dynamic_cast "включается" только для полиморфных классов. RTTI здесь значит что приведение типов происходит во время выполнения, результат приведения соотвественно известен тоже во время выполнения.
(Ходят слухи что в некоторых реализациях dynamic_cast берет информацию о типах из таблицы виртуальных функций, куда компилятор может также запихнуть указатель на инфу об иерархии классов во время генерации кода:
http://stackoverflow.com/a/18359906/2170898)

Пример:

class X
{
virtual ~X() {} // виртуальный деструктор
};

class Y : public class X // иерархия X - Y полифорфная, так как в X определен виртуальный деструктор
{};


При вызове new X в свободной памяти будет создан экземпляр класса X.
Графически это можно изобразить так (первая строка - номера ячеек памяти, вторая строка - объект который лежит по указанному адресу):
[10001] [10002] [10003]
[....X....] [..junk...] [..junk...]

Вызов nex Y сначала создает и размещает в памяти объект класса X, а следом за ним объект класса Y.
[20001] [20002] [20003]
[....X....] [....Y......] [..junk...]

Если приводить указатель на базовый класс X к указателю на класс наследника Y, когда в памяти нет объекта типа Y, то dynamic_cast вернет nullptr:

X * ptr_X = new X;
Y * ptr_Y = dynamic_cast(ptr_X); // ptr_Y будет проиницилизирован nullptr

Что вполне логично, так как в памяти нет никакого объекта типа Y, к которому можно было бы обратится - в памяти находится какой-то мусор, который не получается идентифицировать как объект типа Y.
Если то же самое сделать для неполиморфной иерархии классов SuperBase и Derived:

class SuperBase
{};

class Derived : public class SuperBase
{};

То получим ошибку компиляции:
То получим ошибку компиляции:

SuperBase * ptr_SuperBase = new SuperBase; // объеты в памяти: (SuperBase)|(junk)|(junk)
Derived * ptr_Derived = dynamic_cast(ptr_SuperBase); // error C2683: 'dynamic_cast': 'SuperBase' is not a polymorphic type

Даже если сделать приведение вниз по иерархии "корректным":

SuperBase * ptr_SuperBase = new Derived; // объеты в памяти: (SuperBase)|(Derived)|(junk)
Derived * ptr_Derived = dynamic_cast(ptr_SuperBase); // error C2683: 'dynamic_cast': 'SuperBase' is not a polymorphic type

Для ссылок ошибка будет такая же:
SuperBase obj_SuperBase;
Derived & ref_Derived = dynamic_cast(obj_SuperBase); // error C2683: 'dynamic_cast': 'SuperBase' is not a polymorphic type


Приведение вверх по иерархии будет корректным и для ссылок и для указателей:

Derived obj_Derived;
SuperBase & ref_SuperBase = dynamic_cast(obj_Derived); // ok

Derived * ptr_Derived = new Derived;
SuperBase * ptr_SuperBase = dynamic_cast(ptr_Derived); // ok

Unknown комментирует...

Парсер съел шаблонные аргументы dynamic_cast.
Эта же паста с нетронутыми dynamic_cast'aми:
http://pastebin.com/AEwe0AUZ

Даниил комментирует...

Замечательное разъяснение, я из той категории людей которым нужно все досконально изучить прежде чем начать чем-то пользоваться, а объяснения типа *ну это вот, работает так то* не укладываются в голову, большое спасибо за материал)

glueckware комментирует...

Да что же вы пишете, граждане!
Зачем вообще какой-то _cast для приведения вверх по иерархии?
Приведение вверх идет АВТОМАТИЧЕСКИ!
Кошка является животным? является. Не надо никаких dynamic_cast, никаких RTTI, никаких вообще кастов!
Любую кошку можно (и нужно!)БЕЗ ПРИВЕДЕНИЯ подставить везде, где можно подставить животное!
Иными словами, пусть у нас объявлено:
Cat cat;
void feed(Animal *animal);

Тогда мы можем запросто сделать:
feed(&cat);
И все, никаких кастов вообще!


Unknown комментирует...

Имхо стоит ещё упомянуть что dynamic_cast используется для приведения по иерархии классов в которой есть виртуальное наследование, иерархия должна быть полиморфной.
Пример:
http://pastebin.com/gckcMEfP
Flies away.

Анонимный комментирует...

А есть еще один нюанс, который успешно "глотается" компилятором и даже какое-то время работает:

char *str = "hello";
str[1] = 'a';
cout<<str<<endl; // hallo

Соль: изначально указатель неконстантен и не на константу, но указывает на строковый литерал. "Undefined behaviour" в данном случае такой: какое-то время программа обычно работала, но при частых попытках записи падала.

Впервые заметил эту "фичу" где-то в 2003, по неопытности счел за баг компилятора.

Unknown комментирует...

Найс читаю статью, которая вышла за несколько месяцев до моего рождения...
Удачи!