Компилятор Forte от Sun в меньшей степени, чем компилятор IBM VisualAge для C/C++, соответствует стандарту ANSI/ISO C. Это различие может стать причиной проблем при портировании приложений с Solaris на AIX. Особенно сложной проблемой является наличие разных механизмов управления временными переменными, реализованных в этих двух компиляторах. Данная статься посвящена этой проблеме и возможным способам ее решения.
Большинство корректно написанных C и C++ программ компилируются и запускаются на AIX без изменений. Под корректно написанными программами понимаются те, которые удовлетворяют следующим критериям:
- Программа соответствует стандарту ANSI/ISO C.
- На всех этапах цикла разработки ПО (проектирования, реализации и обслуживания) особое внимание уделялось переносимости приложения.
- В программном коде активно используются прототипы функций.
Корректировка специфичных для конкретного компилятора фрагментов исходного кода - весьма сложная и трудоемкая процедура. Некоторые программисты быстро "сдаются" и начинают во всех бедах обвинять компилятор, не задумываясь над тем, не является ли корнем проблемы некорректно написанный код. Давайте взглянем правде в глаза - соблюдать стандарты трудно, и в большинстве случаев, когда возникает проблема, программисты не догадываются, что пишут нестандартный программный код C/C++. Если при компиляции не возникло ошибок, то это еще не означает, что поведение исполняемого кода будет корректным, так как компилятор может не соответствовать последним стандартам.
Мы приведем несколько примеров, которые показывают ошибки, связанные с неправильным употреблением временных переменных, и продемонстрируем различия в обработке компиляторами временных переменных. Также мы покажем, как использовать стандартные правила программирования для предотвращения этих проблем.
Неправильное использование временных переменных, пример 1
Предположим, что необходимо портировать этот блок кода из Solaris в AIX:
#include <strings.h>
#include <iostream.h>
class Obj
{
private:
char* ptr;
public:
Obj();
~Obj();
operator const char*() const;
Obj& operator=(const Obj&);
Obj(const Obj&);
};
Obj::Obj()
{
cout << "Obj::Obj() this=" << (int)this << endl;
ptr = strdup("hello world");
}
Obj::Obj(const Obj& rhs)
{
cout << "Obj::Obj(const Obj&) this=" << (int)this << endl;
ptr = rhs.ptr ? strdup(rhs.ptr) : 0;
}
Obj& Obj::operator=(const Obj& rhs)
{
cout << "Obj::operator=(const Obj&) this=" << (int)this << endl;
ptr = rhs.ptr ? strdup(rhs.ptr) : 0;
return *this;
}
Obj::~Obj()
{
cout << "Obj::~Obj() this=" << (int)this << endl;
delete ptr;
}
Obj::operator const char*() const
{
cout << "Obj::operator const char*() this=" << (int)this << endl;
return ptr;
}
Obj func()
{
cout << "in func" << endl;
Obj obj;
cout << "obj created in func" << endl;
return obj;
}
main()
{
cout << "in main" << endl;
const char *val = func();
cout << "val=" << val << endl;
} |
Для понимания проблемы необходимо понять, что компилятор делает со строкой "const char val = func();". func() возвращает значение экземпляра объекта Obj. Чтобы сделать это, компилятор создает временную переменную для хранения возвращаемого значения. Другой тонкий аспект состоит в том, что компилятор генерирует вызов из временного объекта к Obj::operator const char *, который принимает копию указателя, ptr, и присваивает ему значение val.
Выходные данные компилятора Solaris Forte
Ниже представлены выходные данные компилятора Solaris Forte:
>/opt/bin/CC testptr.cpp >a.out in main in func Obj::Obj() this=-4261612 obj created in func Obj::Obj(const Obj&) this=-4261500 Obj::~Obj() this=-4261612 Obj::operator const char*() this=-4261500 val=hello world Obj::~Obj() this=-4261500 |
Выходные данные показывают, что деструктор временной переменной (типа Obj) вызывается после последней строки пользовательского кода в программе.
Выходные данные компилятора AIX VisualAge C++
Теперь сравним выходные данные компилятора Solaris с выходными данными компилятора VisualAge C++:
>xlC testptr.cpp >a.out in main in func Obj::Obj() this=804398976 obj created in func Obj::operator const char*() this=804398976 Obj::~Obj() this=804398976 val= ? |
В данном случае деструктор временной переменной вызывается после строки, в которой временная переменная была создана. Проблема возникает в том случае, если переменная, предназначенная для хранения возвращаемого значения и вызываемая функцией func(), удалена до того как она должна была использоваться в качестве указателя, который, в свою очередь, извлекался при вызове оператора const char*(). Такое поведение временной переменной соответствует стандарту. В разделе 12.2, Temporary Objects, пункт 3, стандарта ANSI/ISO C говорится: "… Временные объекты уничтожаются на последнем этапе оценки законченного выражения, которое (лексически) содержит место, где эти объекты были созданы." (см. http://www.ansi.org).
Данная проблема решается несколькими способами. Простейшим способом является перемещение момента создания временной переменной в ту строку, где переменная необходима:
main()
{
cout << "in main" << endl;
cout << "val=" << (const char*)func() << endl;
} |
Есть и другой способ скорректировать некорректную работу кода. Присвойте результат вызова реальному объекту и используйте этот объект для того чтобы избавиться от необходимости наличия временного значения. Результат вызова метода func() присваивается объекту val, который продолжает существовать до конца выполнения метода main. Фактически использование этого способа решения проблемы с компиляторами IBM исключает необходимость во временной переменной. Вызов func() создает экземпляр val объекта Obj:
main()
{
cout << "in main" << endl;
Obj val = func();
cout << "val=" << val << endl;
} |
Неправильное использование временных переменных, пример 2
В этом примере приложение определяет свой собственный строковый класс:
>cat myString.h
#include <string>
#define MY_SL_STD(MY_NAME) ::std::MY_NAME
class MYString
{
public:
// стандартные конструкторы
MYString() {}
MYString(const MY_SL_STD(string&) data) : data_(data) {}
MYString(const MYString& str) : data_(str.data_) {}
MYString(char c, size_t N) : data_(N, c) {}
MYString(const char* s) : data_(s) {}
MYString(const char* s, size_t N) : data_(s,N) {}
// специальный конструктор на базе char
MYString(char c) : data_(1,c) {}
~MYString() {}
const char* data() const { return data_.c_str(); }
// преобразование типов:
operator const char*() const { return data_.c_str(); }
protected:
MY_SL_STD(string) data_;
};
>cat myString.C
#include <iostream.h>
#include "mystring.h"
class A
{
public:
A(const char * str_)
{
strcpy(str, str_);
}
MYString getStr()
{
return str;
}
void print()
{
cout<<"object A " << str <<endl;
}
private:
char str[2000];
};
void foo(const char* s)
{
cout<<"foo: "<<s<<endl;
}
int main()
{
A a("This is a test");
a.print();
const char * p = (const char*) a.getStr();
cout << "p=" << p << endl;
return (0);
} |
Выходные данные компилятора Solaris Forte
Ниже представлены выходные данные компилятора Solaris Forte:
>CC myString.C >a.out object A This is a test p= This is a test |
Выходные данные компилятора AIX VisualAge
Теперь сравним с выходными данными компилятора AIX VisualAge:
>xlC myString.C >a.out object A This is a test p= e@ |
В этом примере массив, состоящий из элементов типа char, в объекте типа object конвертируется в MYString, причем доступ к этому массиву осуществляется через метод getStr. Метод A::getStr создаст объект A::MYString. Всякий раз, когда используется getStr, программа генерирует неправильные результаты, тем самым указывая на проблему.
Когда вызывается getStr(), этот метод берет буфер str и создает объект MYString. Возвращается объект типа MYString, и далее программный код вызывает метод MYString для конвертации в тип const char *. Этот метод возвращает указатель на временную копию строки "This is a test". Как только указатель был получен и записан в переменную p, для временного объекта вызывается деструктор. Теперь область памяти, на которую ссылается p, не содержит "This is a test." Так как строка символов является временной копией, она существует только в исходном местоположении внутри объекта. При любом использовании p будет выполнено обращение к "мусору" (garbage).
Поскольку для создания этих временных объектов используется динамическая память, нельзя зависеть от объекта, существующего дольше, чем выражение, в котором он используется.
Для решения этой проблемы есть несколько путей:
int main()
{
A a(This is a test");
a.print();
cout<<"Problem= "<<(const char*) a.getStr()<<endl;
MYString testStr = a.getStr();
cout<<"(const char *) "<<(const char*) testStr<<endl;
cout<<"testStr " <<testStr<<endl;
cout<<"data() "<<testStr.data()<<endl;
foo((const char *) a.getStr());
return (0);
} |
Для объекта testStr деструктор временного объекта вызван в точке следования (sequence point) - точка с запятой в конце выражения - после выполнения операторов cout. Применение testStr приводит к созданию нового объекта, который будет существовать до конца выполнения функции и поэтому может использоваться где угодно.
Выходные данные будут подобными следующим:
>xlC myString.C >a.out object A This is a test Problem= This is a test (const char *) This is a test testStr This is a test data() This is a test foo: This is a test |
Различия, создаваемые компилятором, из всех проблем часто являются самыми сложными и запутанными. Первой реакцией в такой ситуации является "сваливание" всех проблем на компилятор и его поставщика, а не анализ ошибок самого кода. В этой статье мы описали две похожие ситуации, которые показывают, как различные компиляторы реализуют управление временными переменными. Мы также объяснили, как исправить эти проблемы с некорректной работой при помощи стандартных подходов в программировании.
- Примите участие в обсуждении материала на форуме.
- Solving coding problems with temporary variables when porting applications from Solaris to AIX: оригинал статьи (EN).
- Раздел developerWorks AIX и UNIX содержит сотни информативных статей для читателей начальной, средней и высокой квалификации.
- Новичок в AIX и UNIX?: страница AIX и UNIX для новичков.
- AIX 5L Wiki: совместная разработка документации AIX.
- Разделы библиотеки информации по темам AIX и UNIX:(EN)
- Системное администрирование
- Разработка приложений
- Производительность
- Переносимость
- Безопасность
- Подсказки
- Инструментальные средства и утилиты
- Java™-технологии
- Linux®
- Open source
- IBM trial software: ознакомительные версии программного обеспечения для разработчиков, которые можно загрузить прямо со страницы сообщества developerWorks.(EN)
- Safari bookstore: сайт магазина книг по ИТ.(EN)
- developerWorks blogs: участвуйте в жизни сообщества developerWorks.(EN)
- Примите участие в форумах AIX и UNIX:(EN)
- Управление кластерными системами
- Поддержка IBM
- Инструменты управления производительностью - технический форум
- Виртуализация - технический форум
- Команда IBM developerWorks проводит по всему миру сотни бесплатных технических консультаций.(EN)
- Podcasts: аудиозаписи презентаций экспертов IBM.(EN)
Нам Кеунг (Nam Keung) -- старший программист, работавший в области развития AIX коммуникаций, AIX multimedia, разработки SOM/DSOM и производительности java. В настоящее время его назначение -- помощь ISV в разработке приложений, внедрение приложений, настройка производительности и обучение платформе IBM pSeries. Он работает программистом в IBM с 1989. Связаться с ним можно по адресу namkeung@us.ibm.com.
Говард Насгаард (Howard Nasgaard) - аналитик IBM в Toronto Software Development Lab, консультирующий программистов. Он занимается разработкой программного обеспечения 18 лет, последние семь лет ведет разработку компилятора C++. В настоящее время он является инженером-разработчиком внешнего интерфейса C++. Связаться с Говардом можно по адресу nasgaard@ca.ibm.com.
Больше чем 10 лет Брэд Кобб (Brad Cobb) помогал поставщикам решений портировать, настраивать, отлаживать и улучшать их приложения в средах разработки IBM AIX. Кроме помощи поставщикам решений, Брэд регистрировал патенты, публиковал статьи и выступал на конференциях разработчиков. Вы можете написать ему на адрес bcobb@us.ibm.com.