четверг, октября 02, 2008

Приключение со строками

Злосчастный код, который похитил у меня несколько часов времени.

char c='0';
string str="Text"+c;

Что будет лежать в str? Отнюдь не "Text0" как можно было бы ожидать.

Здесь к адресу строки "Text" добавляется 48 байт. И конструктор str получает указатель на эту область памяти. По несчастному стечению обстоятельств в этой области памяти у меня была строка "RESTART". И вот с этого момента начались удивительные приключения этого RESTART'а, которые в итоге привели к обращению по нулевому указателю, причины которого мне и пришлось раскапывать.

Компиляторы, которые я смотрела - CodeWarrior и Visual Studio 2005 даже ворнинга в этом случае не дают.

57 коммент.:

Андрей Валяев комментирует...

Это еще раз подтверждает тот факт, что когда работаешь на C++ не стоит связываться с примитивными типами. :)

string str = string("Text") + c;

ИМХО сработает как надо, хотя не проверял. Надо вырабатывать у себя привычки работать с объектами.

PS: Ни в коем случае не хочу никого обидеть.

Alexey Yakovenko комментирует...

это еще раз подтверждает тот факт, что перегруженные операторы C++ вырабатывают дурацкие привычки.
а потом за такими код вычищать неделями приходится.

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

нудык ктож к const char * прибавляет char ?
))))

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

Соглашусь с waker. Я вообще на plain C пишу ;-)

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

GCC может быть предупредил бы.

Еще Студия, не выдает предупреждения если написать что-то вроде
printf("%s", myClass);
GCC же в этом случае говорит почти по-русски, что тут использовать не POD-типы нельзя.
Я согласен, кстати с Андреем. Еще лучше не использовать типы int или short или long, а использовать что-то вроде Int16, UInt32, size_t, и т.п.

Андрей Валяев комментирует...

2arkanoid: Ну дык тем, кто пишет на си, никогда не придет в голову складывать строки. :)

А С++ предоставляет для этой цели ИМХО достаточно удачную абстракцию. И это балует. :)

string text = "Text";
string srt = text + c;

тоже не создал бы проблем, но тяжелое наследие си, в виде арифметики над указателями, мешает это интуитивно совместить. :)

2waker: как объединяют строки типа string у вас?

Денис Радченко комментирует...

Я еще раз порадовался, что перешел на PHP/Javascript.
Велик и могуч C++, но чем более он велик, тем больше возможность совершить подобную ошибку

Андрей Валяев комментирует...

gcc тоже никак на это не ругается.

Для любого компилятора что char что int - одно и тож.

2naishin: Я не очень люблю замещать long, int, short - последний нужен крайне редко, а первые два и так достаточно понятны. Хотя это имеет смысл при настоящем кроссплатформенном программировании.

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

Я, конечно, дико извиняюсь, но возможностей набагать в PHP примерно столько же, сколько и в C++, то есть немеряно.

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

недавно был очень похожий случай.
есть функция:
string addsign(char sign, const string& s)
{
1) return sign+s; // так нельзя
2) return string(s1)+s; //тоже нельзя
3) return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
}

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

Если очень хочется писать "Text"+c, лучше определить враппер над char'ом и обозвать его, скажем, ANSI_CHAR.
Затем добавить глобальный оператор который принимает строку (на стеке) как lhs и конст-реф ANSI_CHAR как rhs.

Тогда код string str="Text"+c; превратится в создание строки с текстом "Text" и вызов нашего оператора +.

А использование голых си-строк запретить в кодинг стандарте и жестоко бить нарушителей :)

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

правда, вышеописаный способ всё равно не поможет от str = "Text" + '0';

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

Да... Увы, но потихоньку начинаем забывать, что С++ хоть и с двумя плюсами, но все равно С, и все, что свойственно С в нем остается.
Попробуйте тот же код на C компиляторе (без всяких ++) - и увидите, что будет и Warning (а то и error в зависимости от конкретного экземпляра компилятора. Разбираем? OK!

char c='0';
/*
Ну, тут собственно и писать не о чем... Инструкция полностью эквивалентна
BYTE c=0x30; // Илиже 84 десятичное
*/
string str="Text"+c;
/*
А тут уже интереснее "Text" вполне себе константная строка (которая кстати говоря заканчивается двоичным нулем).
Тип string - хм... Это 100% char*. Да, я понимаю, очень хотелось, чтоб это был класс, но увы, при таком объявлении - это именно тип и здается мне, что компилятор его понял именно как char* (кстати, компилятор прав).
Итак получается, что компилятор видит полны эквивалент кода
char *str = (char *)("Text") + 0x30;
Результат - ровно тот, который Вы описали... Т.е. указатель на байт по адресу (Начало строки "Text" + 48)
*/

Более чем уверен, что Вы до этого додумались и сами, но комменты насторожили... Решил таки расписать... Прошу прощения если кого-нить ненароком обидел. Видит бог это не со зла...

Андрей Валяев комментирует...

Все даже еще проще, в данном случае слева от знака равно стоит один тип, а справа два других типа.

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

Когда мы одно из слагаемых приводим к нужному типу - то тип результата получается ожидаемый.

Так тоже работает:
string str = "Text" + string(1, c);

Хотя конструирование строки из char - менее доходчиво.

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

Да нужно пользоваться всегда += с единственной переменной справа и никаких проблем не будет!

Sergey Kishchenko комментирует...

Используйте стримы или boost::lexical_cast, что в принципе одно и то же :) Суммирование строк - дурная затея. А суммирование строк и char - это вообще некрасивей некуда.

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

2Сергей Кищенко:

Используйте стримы или boost::lexical_cast, что в принципе одно и то же

Это, возможно, неплохой совет если речь идет о работе на PC и не об играх.
В играх обычно не используется boost, исключения еще не используются, потому что считается, что это тормозно очень. Нет, ну возможно где-то они и используются, но общие настроения такие. Мы же сейчас не на PC работаем, у нас тут скорости не те, да и работа с памятью довольно специфическая.

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

Пакаль (делфи) рулит =)

Андрей Валяев комментирует...

2Алёна: А string в gamedev не считается чем-то тяжелым? и вобще STL. :)

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

как на счет strncat("Text",'0',1) ?
в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.
Mario.

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

2Street:
Пакаль (делфи) рулит =)

Интересное замечание, особенно в сочетании с предыдущим комментарием. :-)
Мне вообще не известны консоли, к которым есть компилятор Паскаля. С/C++, ну ассемблер свой еще. Вот и всё, пожалуй.

2Андрей Валяев
2Алёна: А string в gamedev не считается чем-то тяжелым? и вобще STL. :)

Хех, я разглядела иронию :-). И тем не менее - считается. Но без STL живется совсем хреново. Поэтому он либо таки используется, но очень аккуратно, с пониманием того, как он устроен внутри. Иначе начнет вектор память переаллоцировать в самые неудачные моменты времени или еще чего...
Если у компании есть время и деньги разрабатывается свой STL. Например я когда-то писала про EASTL. Вот тот string, который участвует в примере этого поста, это не STL'евский string на самом деле. Правда тут это не важно, потому что в такой ситуации они ведут себя одинаково.

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

2Анонимный:
как на счет strncat("Text",'0',1) ?
в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.
Mario.


Да тут много возможных вариантов разруливания ситуации, с явным вызовом конструктора string уже был вариант.

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

Извините за нескромный вопрос. А зачем такой "загадочный" код может понадобиться?

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

2Анонимный:
Извините за нескромный вопрос. А зачем такой "загадочный" код может понадобиться?

В упрощенном виде ситуация такая: считывание массива значений из текстового конфига.

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

На мой взгляд с таким кодом лучше бороться "архитектурным" способом а не программным :) Т.е. никаких текстовых конфигов и парсинга в игровом коде. А во время сборки игры использовать потоки или что-нибудь еще высокоуровневое и потенциально устойчивое к ошибкам.

Предложу свой кривой вариант
string s( "Text_" );
*s.rbegin() = '1';

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

2Алёна:
да уж случай со строкой нечастый.
Boost конечно лучше применять с умом. Некоторые его вещи вполне подойдут для игр boost::shared_ptr, boost::any. А boost::lexical_cast полный тормоз. лучше snprintf пока-еще ничего нет, и быстро и переносимо. Со своим Wraper'ом уходит и последний минус snprintf - отсутствие проверки типов().
STL просто незаменим но тут спорный вопрос вот в Crysis используется STL Port а в Unreal свои контейнеры. Я видел коммерческий проект где написаны свои контейнеры, все настолько убого, тормознуто и с кучей ошибок, что без слез смотреть нельзя, хотя автор кода человек с законченными проектами и с большим опытом. К примеру в своей строке утечка памяти. В Сортировка пузырьком и в Critical Time коде и везде. "STL там не применяется по историческим причинам"

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

= считывание массива значений из текстового конфигаига =

Не сильно в теме, но - а как же считывают значения из текстового конфига в линуксе? Неужели нет готовых решений?

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

за-то можно так извратиться

char *s = "qweqweqwe";
int a=1;
cout << a[s] ;

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

Яркий пример точек следования:

решение всегда проще
char c='0';
std::string str="Text";
str =str +c;
std::cout<< str;

0ptr комментирует...

> Вот тот string, который участвует в примере этого поста, это не STL'евский string на самом деле. Правда тут это не важно, потому что в такой ситуации они ведут себя одинаково.

Так в этой ситуации же неважно, одинаково ведут или нет, и вообще string не причём - происходит-то всё в правой части выражения, и никак не зависит от левой. Алёна, Вы же наверняка это прекрасно понимаете, зачем же других путать? Теперь некоторые могут подумать - фу, какая гадость этот ваш string, а он вроде и не виноват ни разу :)

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

Мне вообще не известны консоли, к которым есть компилятор Паскаля.вообще gcc есть под некоторые консоли. под wii и ds — 100%. поэтому при большом желании можно и на паскале, и на фортране, и наверное даже на аде :)

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

2n2s:
Так в этой ситуации же неважно, одинаково ведут или нет, и вообще string не причём - происходит-то всё в правой части выражения, и никак не зависит от левой. Алёна, Вы же наверняка это прекрасно понимаете, зачем же других путать?

Тут важно, что конструктор string'а все так же принимает char*. Ну и то, что любые варианты решения, которые работают с STL'евским string'ом будут работать и тут.

0ptr комментирует...

Алёна,
Не понимаю, "всё так же принимает char*" - это в каком смысле?

Выражение "Text"+c имеет тип const char*, так же как и сам "Text". Поэтому если str="Text" работает, то и поведение str="Text"+c будет однозначным. Разве нет?

Андрей Валяев комментирует...

Что-то переливаем из пустого в порожнее.

Поведение char *, который прочно ассоциируется со строкой, при сложении для программиста C++ кажется неестественным.

Програмисты си легко видят здесь ошибку, потому что в си нет operator +. А программист C++ начинает сразу задумываться, что куда к чему приводится для сложения, и какое сложение здесь может быть использовано. :)

Поэтому лучше использовать string сразу, чтобы меньше думать. :)

Alex Ott комментирует...

2Сергей Кищенко:
boost-вские библиотеки очень хороши для прототипирования, но потом надо используемые классы/функции переписывать - проверено на опыте. lexical_cast очень тормозной. boost::any - удобный класс, но я после замены его на union получил почти 25% повышение производительности...

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

2 n2s:
Поэтому если str="Text" работает, то и поведение str="Text"+c будет однозначным. Разве нет?

Ну мало ли какой у нас string и чем он отличается от стандартного. Может, он имеет ограничение на длину в 5 символов или какие-нибудь другие экзотические ограничения.

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

2zg:
вообще gcc есть под некоторые консоли. под wii и ds — 100%.

Искала - не нашла. Можешь дать ссылку?

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

Многие пишут что Сишник бы такое не написал :) или те кто раньше писал и пишет на ассемблере + си.... ибо для них как и для меня собственно оно выглядит иначе.... что такое строка-константа "Text" - это число обычно число ... что такое '0' - тоже число ... и не более обычные числа... их сложить можно ... при этом будет новое число... но дальше мы уже работаем с этим числом как с адресом на строку ... и конечно же получается что адрес на строку указывает вовсе не туда куда якобы хотелось :) Си++ развращает программистов ИМХО

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

Между прочим, даже g++ -Wall не выдает предупреждений, ибо формально это совершенно правильный код: справа работает арифметика указателей...

Андрей Валяев комментирует...

2Алена: gcc работает везде! :)

Может эта ссылка окажется полезной?

Андрей Валяев комментирует...

2saabeilin: С точки зрения компилятора код правильный. но с точки зрения логики - нет. И компилятор мог бы предупретить по хорошему.

Арифметика указателей логична при использовании переменной. Но когда строка задается явно - любая арифметика ошибочна скорее всего, потому что в результате вычислений указатель на оригинальную строку пропадает. И какой тогда смысл хранить ее в исполняемом модуле как есть?

gcc например предупреждает, когда printf вызывается с некорректным количеством аргументов или с несоответствующими их типами. Хотя с точки зрения языка на три точки можно передать что угодно!

Здесь почти та же ситуация.

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

2 Андрей Валяев: согласен, почему и был весьма удивлен отсутствием предупреждений (gcc 4.1.2, но подозревая что и 4.3 поведет себя аналогично, нет под рукой)

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

Искала - не нашла. Можешь дать ссылку?да как бы и не секрет: devkitpro.org
это уже собранное с либами и т.д.
а так в gcc таргеты powerpc и arm присутствуют официально: http://gcc.gnu.org/install/specific.html

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

>> А string в gamedev не считается чем-то тяжелым? и вобще STL. :)
>Хех, я разглядела иронию :-). И тем не менее - считается. Но без STL живется совсем хреново.

Я вот работаю в проекте, в котором нельзя ни STL, ни исключений. Есть, правда, замена такая, в виде набора контейнеров собственного авторства. Строки есть юникодовые, векторы, мапы. Не STL, конечно, но жить можно.

А классы строк, вон вообще все кому не лень пишут. Бери любой да пользуйся.

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

Мде, прочитал комментарии на простой в общем случай. Кстати, кто еще хорошо помнит Builder C++ версии 3.5, тот который под DOS :) такие ошибки не допускает, с другой стороны подобные вещи сразу в глаза могут и не бросаться. Да и вообще, не ошибается только тот, кто ничего не делает.
А чтобы такого не было нужно использовать managed код :). При работе со строго типитизированными классами таких проблем не будет :)
Кстати, а если выставить повыше уровень предупреждений Warning - неужели компилятор не ругнется? (правда в этом случае он будет ругаться на все библиотеки Microsoft, но это легко обходится)

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

2Clevelus:
Кстати, а если выставить повыше уровень предупреждений Warning - неужели компилятор не ругнется?

У меня сейчас под рукой только VC++7.1. Проверила в нем - не ругается.

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

2zg:

да как бы и не секрет: devkitpro.org
это уже собранное с либами и т.д.
а так в gcc таргеты powerpc и arm присутствуют официально: http://gcc.gnu.org/install/specific.html


угу, спасибо

time.technician комментирует...

Хе-хе. Прикольно.

std::string str = "Text" + c;

В чём тут ошибка? Просто в понимании того, что определяет тип операции.
1) компилятор анализирует типы и обнаруживает
string = const char* + char
2) что получаем теперь при вычислении?
В первую очередь у нас будет вызван operator+ для встроенного типа const char*...
И только потом он вызовет копирующий конструктор и создаст объект типа std::string.

Это не ошибка программиста, развращённого C++, нет. Это особенность ООП-подхода конкретно в языке: вызываемый оператор определяется не возвращаемым типом, а левым операндом. И всего-то :)

Варианты:

#include <iostream>

int main ()
{
 { //исходный вариант
  char c = '0';
  std::string str = "Text" + c;
  
  std::cout << str << std::endl;
 }

 { //предпочитаю так
  char c = '0';
  std::string str = "Text";
  str += c;
  
  std::cout << str << std::endl;
 }

 { //но можно и так
  char c = '0';
  std::string str = std::string("Text") + c;
  
  std::cout << str << std::endl;
 }

 return 0;
}

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

Забавно!
недавно был очень похожий случай.
есть функция:
string addsign(char sign, const string & s)
{
1) return sign+s; // так нельзя
2) return string(s1)+s; //тоже нельзя
3) return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
}

Решил это сам проверить, на компиляторе gcc 3.4.2.
вот такая программулинка:

#include <string>
#include <stdio.h>
using namespace std;
string addsign(char s1, const string & s)
{
//1)return s1+s; // так нельзя
//2)return string(s1)+s; //тоже нельзя
//3)return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь
//4)return string(&s1, 1)+s;
}
int main(int n, char **args)
{
string text = "1.9876";
string res = addsign('-', text);
puts(res.c_str());
return 0;
}

выдала мне срвсем другое:
первый вариант (как "нельзя") выдал правильный результат: -1.9876, второй вариант - ошибку компиляции, а третий(который "только так и можно") дал нечто невразумительное - кучу смеющихся рожиц и в конце-число 1.9876. Мне даже непонятно, как у автора сообщения получился нормальный результат.
Последний (4-ый, "мой" вариант) дал тоже верный результат: -1.9876

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

Малость ошибка закралась в Ваши рассуждения :)

string addsign(char s1, const string & s)
{
//1)return s1+s; // так нельзя

тут то как раз все можно, в STL есть оператор +, который в качестве одного из операндов принимает char, второго - string

//2)return string(s1)+s; //тоже нельзя

ага, компилятор ругается, нет такого конструктора у string

//3)return string(s1, 1)+s;// только так можно... но блин не сразу допетриваешь

все замечательно, только параметры у конструктора перепутали, в итоге создается строка из s1 символов с кодом 1 (ага, тех самых, смеющися рожиц :) )

//4)return string(&s1, 1)+s;
}

тут тоже все хорошо - string создается по одному символу из строки &s1

проверялось на VC8(SP1)

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

вас stl'щиков в утиль давно пора.
если лень писать свои классы, чтобы были шустрые и расчитанные на мультипроцессор, пишите на C#.
микрософт так старались appdev от c++ отделить, а вы все ещё кипятите.

оставьте c++ для системных прогеров :)

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

да btw, советую почитать ISO стандарт c++.
по стандарту код

char c='0';
string str="Text"+c;

это

class char c = __int32(0x30)
class string str = __int32(char*("Text")) + __int32(c);

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

longlong n;
long t = 0;
n = t -1;

что будет в n? -1? ага три раза оно там будет. там будет 0xffffffff
что для longlong не -1.
на х86 это нормально, а вот х64... таких программистов надо растреливать.

char c='0';
string str = string("Text")+c;

приведение типов спасет этот мир.

Андрей Валяев комментирует...

Помоему Алёне давно пора закрывать эту дискуссию, а комментирующим не мешает читать впередиидущие комментарии.

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

2Андрей Валяев:

Помоему Алёне давно пора закрывать эту дискуссию, а комментирующим не мешает читать впередиидущие комментарии.

Да уж, а то разговор пошел по кругу.
И 53 комментария на две строки кода - это как-то многовато.
Давайте завершим на этом.

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

Тут упоминали php, а в нём кокатенация строк происходит через оператор "точка".

$a= "10Text".1; // $a= "10Text1";
$b= "10Text"+1; // $b= 10+1;

Через + происходит сложение чисел.

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

Проблема ясна. Символ должен быть символом, а байт - байтом )

Андрей Валяев пишет...
string str = string("Text") + c;
Создаем временную переменную (вызываем конструктор), вызываем перегруженный operator+, вызываем перегруженный operator=, удаляем временный объект (вызываем деструктор)...

Mario пишет...
как на счет strncat("Text",'0',1) ?
в итоге к "Text" будет присоединен '0', добавлен '\0' в конце и возвращен указатель на начало строки.

Во-первых, не strncat("Text",'0',1), а strncat("Text","0",1)...
Во-вторых, запись в const-секцию (которая read-only)? Тогда вызови для начала VirtualProtect... И убедись, что после "Text" не лежит "Text2".

coff

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

Еще раз убеждаюсь что нужно начинать учить с основ. Вы будете удивлены сколько времени еще сэкономите, я обещаю.
Например я подобный вопрос на собеседовании задаю, в качестве отсева по телефону.