Closed alexey-malov closed 8 years ago
CMyString::CMyString(const char * pString, unsigned length)
{
assert(pString);
m_size = length;
m_chars.reset(new char[m_size + 1]);
memset(m_chars.get(), '\0', m_size + 1);
memcpy(m_chars.get(), pString, m_size);
}
зачем дважды заполнять массив, когда можно скопировать m_size байт и в конце добавить '\0'
CMyString const CMyString::SubString(unsigned start, unsigned length /*= UINT_MAX*/) const
{
if (start + length > m_size)
{
return *this;
}
return CMyString(m_chars.get() + start, length);
}
на 64 битной платформе будет разная разрядность типов unsigned и size_t. Символы строки свыше 4Гб будут недоступны для этой функции используй size_t для start и length
CMyString("Hello").SubString(3, 5)
вернет "Hello", что не вяжется с ожиданиями
Сделай в тестах разбор возмоных ситуаций. за эталон бери STL
void CMyString::Clear()
{
m_chars.reset(nullptr);
}
После этого GetStringData() будет возвращать нулевой указатель, что приведет к неопределенному поведению при использовании. Кроме того, не будут выполнен инвариант "длина строки равна нулю"
Для каждого метода класс String выпиши следующее: -предусловия (что ожидаем от аргументов) -постусловия (возвразащемый результат и ожидаемое состояние объекта) вызов каждого метода должен переводить объект из одного валидного состояния в другое. методы, бросающие исключение должны предоставлять строгую гарантию безопасности исключений. В худшем случае - базовую.
CMyString::CMyString(const char * pString, unsigned length)
{
assert(pString);
m_size = length;
m_chars.reset(new char[m_size + 1]);
memcpy(m_chars.get(), pString, m_size);
m_chars.get()[m_size] = '\0';
}
как на 64-битной платформе передать строку длиннее 4Гб?
Тесты неструктурированы. Особенно тесты конструктора. Там даже проверяется Substr
{
CMyString str("cat");
const char *data = str.GetStringData();
BOOST_CHECK_EQUAL(data, "cat");
BOOST_CHECK_EQUAL(data[str.GetLength()], '\0'); // предыдущий Check интерпретирует строку как zero-terminated, и не прошел бы в случае отсутствия символа с кодом 0
}
{
CMyString str("catdog", 3);
const char *data = str.GetStringData();
BOOST_CHECK_EQUAL(data, "cat");
BOOST_CHECK_EQUAL(data[str.GetLength()], '\0');
}
CMyString str("cat", 3);
const char *data = str.GetStringData();
BOOST_CHECK_EQUAL(data, "cat");
BOOST_CHECK_EQUAL(data[str.GetLength()], '\0');
}
``` аналогично
{
CMyString str("cat", 10);
const char *data = str.GetStringData();
BOOST_CHECK_EQUAL(data, "cat");
BOOST_CHECK_EQUAL(data[str.GetLength()], '\0');
}
этот тест может привести к неопределенному поведению (выход за пределы массива). Нужно сделать так:
CMyString str("cat\0dog\0pie", 10);
содержимое сравнить через memcmp с "cat\0dog\0pi\0"
CMyString str;
BOOST_CHECK_EQUAL(str.GetStringData(), "");
длину проверить.
Желательно запилить вспомогательную функцию для сравнения.
template <size_t N>
void ExpectZeroTerminatedStringData(CMyString const& s, const char (&data)[N])
{
static_assert(N > 0, "Non-zero array is expected");
BOOST_REQUIRE_EQUAL(s[N - 1], '\0');
BOOST_REQUIRE_EQUAL(s.GetLength(), N - 1);
BOOST_REQUIRE_EQUAL(memcmp(s.GetStringData(), data, N), 0u);
BOOST_REQUIRE_EQUAL(s.GetStringData()[s.GetLength()], '\0');
}
Вместо
CMyString str("cat");
const char *data = str.GetStringData();
BOOST_CHECK_EQUAL(data, "cat");
BOOST_CHECK_EQUAL(data[str.GetLength()], '\0');
достаточно написать
ExpectZeroTerminatedStringData(CMyString("cat"), "cat");
существующие проверки заменить на подобную функцию
CMyString const CMyString::SubString(size_t start, size_t length /*= UINT_MAX*/) const
{
if (start >= m_size)
{
return CMyString();
}
if (start + length > m_size)
{
return CMyString(m_chars.get() + start, m_size - start);
}
return CMyString(m_chars.get() + start, length);
}
лучше возвращать неконстантный CMyString. Это позволит использовать перемещающий конструктор или оператор присваивания.
BOOST_AUTO_TEST_CASE(testSubstring)
{
CMyString str("Hello");
CMyString fer = str.SubString(0, 6);
BOOST_CHECK_EQUAL(fer.GetStringData(), "Hello");
BOOST_CHECK_EQUAL(str.SubString(3, 5).GetStringData(), "lo");
BOOST_CHECK_EQUAL(str.SubString(10, 5).GetStringData(), "");
}
использовать ExpectZeroTerminatedStringData, а также проверить граничное условие (start=string.length).
CMyString::CMyString(const char * pString, unsigned length)
{
assert(pString);
m_size = length;
m_chars.reset(new char[m_size + 1]);
memcpy(m_chars.get(), pString, m_size);
m_chars.get()[m_size] = '\0';
}
В тестах рассмотреть граничные случаи: -переданная длина = 0 -в конце строки находится символ с кодом 0
if ((index > m_size) || (index < 0))
{
throw exception("string subscript out of range");
}
в STL есть разные классы исключений. Здесь больше подходит out_of_range Проверка index < 0 всегда false и не имеет смысла. Тип size_t - беззнаковый
BOOST_AUTO_TEST_CASE(testIndexedAccess)
{
CMyString str;
BOOST_CHECK(str[0] == '\0');
BOOST_CHECK_THROW(str[1], std::exception);
CMyString hello("Hello");
BOOST_CHECK(hello[0] == 'H');
BOOST_CHECK_EQUAL(hello[5], '\0');
BOOST_CHECK_THROW(hello[6], std::exception);
}
Этот тест проверяет работу лишь неконстантного оператора [].
CMyString& CMyString::operator+=(const CMyString &other)
{
if (!other.GetLength())
{
return *this;
}
CMyString tmp = *this + other;
*this = tmp;
return *this;
}
В результате *this + other конструируется временный объект Он перемещается в tmp потом копируется в this Лучше перемещать в this сразу, минуя временный объект
CMyString& CMyString::operator=(const CMyString &other)
{
if (this == &other)
{
return *this;
}
m_size = other.GetLength();
m_chars.reset(new char [m_size + 1]);
memcpy(m_chars.get(), other.GetStringData(), m_size + 1);
return *this;
}
если new бросить исключение, то текущий объект будет в невалидном состоянии (m_size хранит новую длину, а указатель m_chars будет указывать на старую строку).
int CompairStrings(const CMyString &leftString, const CMyString &rightString)
сравнить переводится как compare
int CompairStrings(const CMyString &leftString, const CMyString &rightString)
{
size_t sizeOfLeftString = leftString.GetLength();
size_t sizeOfRightString = rightString.GetLength();
size_t cmpSize = (sizeOfLeftString > sizeOfRightString) ? sizeOfLeftString : sizeOfRightString;
return memcmp(leftString.GetStringData(), rightString.GetStringData(), cmpSize);
}
Ты сравниваешь блоки памяти длиной с бОльшую строку, что приводит к выходу за пределы области памяти строки меньшей длины и к неопределенному поведению
CMyString operator +(const CMyString &leftString, const CMyString &rightString)
{
if (leftString.Empty() && rightString.Empty() )
{
return "";
}
size_t sizeOfLeftString = leftString.GetLength();
size_t sizeOfRightString = rightString.GetLength();
size_t length = leftString.GetLength() + rightString.GetLength() + 1;
std::unique_ptr<char[]> chars(new char[length]);
memcpy(chars.get(), leftString.GetStringData(), sizeOfLeftString);
memcpy(chars.get() + sizeOfLeftString, rightString.GetStringData(), sizeOfRightString);
chars.get()[length - 1] = '\0';
return CMyString(chars.get(), length-1);
}
Здесь происходит создание временного массива, копирование в него данных, из которого потом снова данные копируются в CMyString. Избавиться от лишнего копирования.
CMyString::CMyString(CMyString && other)
{
*this = move(other);
}
При отсутствии перемещающего оператора присваивания такой перемещающий конструктор выполняет копирование, а не перемещение.
Автоматически компилятор не сгенерирует в твоем случае оператор перемещения
поскольку в классе есть нетривиальные операторы перемещающего и копирующего присваивания, их тоже следует покрыть тестами. Как и перемещающий конструктор и копирующий конструкторы
BOOST_AUTO_TEST_CASE(after_moving_the_object_must_be_in_a_valid_state)
{
CMyString donor("Hello");
CMyString acceptor("world");
// Operator = must return the reference to the left argument
BOOST_CHECK_EQUAL(&(acceptor = move(donor)), &acceptor);
ExpectZeroTerminatedStringData(acceptor, "Hello");
ExpectZeroTerminatedStringData(donor, "");
// Donor must be in a valid state
donor += "new content";
ExpectZeroTerminatedStringData(donor, "new content");
}
1>------ Build started: Project: TestCMyString, Configuration: Debug Win32 ------
1> TestCMyString.cpp
1> TestCMyString.vcxproj -> F:\teaching\ips\2014\students\baranov\OOP\lab6\CMyString\Debug\TestCMyString.exe
1> Running 14 test cases...
1> unknown location(0): fatal error in "after_moving_the_object_must_be_in_a_valid_state": memory access violation
1> f:\teaching\ips\2014\students\baranov\oop\lab6\cmystring\testcmystring\testcmystring.cpp(17): last checkpoint
1>
1> *** 1 failure detected in test suite "TestsOfCMyString"
Каждый метод/оператор должен переводить объект из одного валидного состояния в то же самое, либо другое валидное состояние. Из всех точках выхода, включая выход по исключению.
Каждый используемый методом объект (передаваемый через аргументы метода, или на который у объекта есть ссылка или указатель (агрегация/композиция), либо глобальный объект) должен либо остаться в том же состоянии, либо перейти в новое валидное состояние.
Валидное состояние - состояние, в котором данные объекта находятся в согласованном состоянии.
Это должно проверяться в тестах
CMyString SubString(size_t start, size_t length = UINT_MAX)const;
На 64-битной платформе UINT_MAX остается 32-битным. Используй SIZE_MAX
CMyString operator +(const CMyString &leftString, const CMyString &rightString)
{
if (leftString.Empty() && rightString.Empty())
{
return "";
}
size_t sizeOfLeftString = leftString.GetLength();
size_t sizeOfRightString = rightString.GetLength();
size_t length = sizeOfLeftString + sizeOfRightString;
CMyString str(leftString.GetStringData(), length);
memcpy((char*)str.GetStringData(), leftString.GetStringData(), sizeOfLeftString);
memcpy((char*)str.GetStringData() + sizeOfLeftString, rightString.GetStringData(), sizeOfRightString);
return str;
}
Преобразование типов в стиле C в прграммах на C++ использовать не следует. Снятие константности с переменной - тоже признак кода с тухлятиной.
CMyString(unique_ptr<char[]> && data, size_t length);
Вариант с += считаю предпочтительнее. если пользоваться этим конструктором, то вызывающая сторона должна не забыть выделить память на 1 символ больше и разместить в конце символ с кодом 0. Дружба без крайней необходимости не нужна
CMyString::CMyString(CMyString && other)
{
*this = move(other);
other.m_chars.reset(new char[1]);
other.m_chars.get()[0] = '\0';
}
new может выбросить исключение. В итоге объект-донор останется в состоянии, не позволяющем его дальнейшее использование (нулевой указатель на данные и невалидная длина)
CMyString& CMyString::operator+=(const CMyString &other)
{
if (other.Empty())
{
return *this;
}
auto chars = move(m_chars);
size_t length = m_size + other.GetLength();
m_chars.reset(new char[length + 1]);
memcpy(m_chars.get(), chars.get(), m_size);
memcpy(m_chars.get() + m_size, other.GetStringData(), other.GetLength() + 1);
m_size = length;
return *this;
}
Если new char[length + 1] выбросит исключение, объект останется в невалидном состоянии (нулевой указатель на данные)
BOOST_AUTO_TEST_CASE(MalovsTest)
{
CMyString donor("Hello");
CMyString acceptor(donor);
CMyString result = "1234";
result += acceptor;
ExpectZeroTerminatedStringData(result, "Hello1234");
}
Для перемещающего конструктора не был написан тест. В итоге, он работает некорректно.
CMyString& CMyString::operator=(CMyString &&other)
{
if (this == &other)
{
return *this;
}
m_chars = move(other.m_chars);
m_size = other.m_size;
other.m_size = 0;
other.m_chars.reset(new char[1]);
other.m_chars.get()[0] = '\0';
return *this;
}
Если оператор new в other.m_chars.reset(new char[1]);
, то объект-донор останется в невалидном состоянии (нулевой указатель на данные).
CMyString& CMyString::operator=(CMyString &&other)
{
if (this == &other)
{
return *this;
}
m_chars = move(other.m_chars);
m_size = other.m_size;
other.m_chars = make_unique<char[]>(1);
other.m_chars.get()[0] = '\0';
other.m_size = 0;
return *this;
}
если new внутри make_unique() выбросит исключение, то other.m_pchars останется равным nullptr и прежней длиной. Попытка использовать его (использовать указатель GetStringData(), который должен возвращать указатель на zero-terminated char array), вернет нулевой указатель, что приведет.
Должны выполняться инварианты как в объекте-доноре, так и в объекте акцепторе:
CMyString::CMyString(CMyString && other)
{
*this = move(other);
other.m_chars = make_unique<char[]>(1);
other.m_chars.get()[0] = '\0';
other.m_size = 0;
}
Конструкторе копирования используется код, повторяющий операции перемещающего оператора присваивания. Если оператор присваивания справился со своей задачей, то нет смысла повторно это делать. Если оператор присваивания не справился с задачей, то до ее выполнения в конструкторе копирования дело не дойдет, т.к. управление будет передано в обработчик снаружи
CMyString& CMyString::operator=(CMyString &&other)
{
if (this == &other)
{
return *this;
}
CMyString temp;
m_chars = move(other.m_chars);
m_size = other.m_size;
other.m_size = 0;
other.m_chars = move(temp.m_chars);
return *this;
}
использование temp здесь излишне, тем более, что при его объявлении может выброситься исключение.
Исправлено на:
CMyString& CMyString::operator=(CMyString &&other)
{
if (this == &other)
{
return *this;
}
m_chars = move(other.m_chars);
m_size = other.m_size;
other.m_size = 0;
return *this;
}
CMyString::CMyString()
{
SetEmptyString();
}
Если вместо SetEmptyString перевести строку в "пустое" состояние (как при перемещении), то этот конструктор не будет требовать никаких выделений памяти и не будет выбрасывать исключений вообще.
Не исправлено
const char* CMyString::GetStringData() const
{
return m_size ? m_chars.get() : "";
}
Проверять корректнее не размер, а указатель m_chars на неравенство nullptr
Исправлено на:
char* CMyString::GetStringDataImpl() const
{
return (m_chars != nullptr) ? m_chars.get() : "";
}
const char* CMyString::GetStringData() const
{
return GetStringDataImpl();
}
void CMyString::Clear()
{
if (m_size)
{
SetEmptyString();
}
}
Если отказаться от SetEmptyString, то можно реализовать noexcept Clear
Исправлено
CMyString& CMyString::operator+=(const CMyString &other)
{
if (other.Empty())
{
return *this;
}
auto chars = move(m_chars); // забрали из m_chars данные (m_chars теперь nullptr)
size_t length = m_size + other.GetLength();
m_chars = make_unique<char[]>(length + 1); // !!!может выбросить исключение
memcpy(m_chars.get(), chars.get(), m_size);
memcpy(m_chars.get() + m_size, other.GetStringData(), other.GetLength() + 1);
m_size = length;
return *this;
}
Если make_unique выбросит исключение, то текущая строка останется в невалидном состоянии (nullptr-указатель на данные и ненулевая длина). Пользоваться строкой будет нельзя. В реализации свести к необходимому минимуму количество выделений памяти и копирования символов:
Заменено на:
CMyString& CMyString::operator+=(const CMyString &other)
{
if (other.Empty())
{
return *this;
}
size_t length = m_size + other.GetLength();
auto chars = make_unique<char[]>(length + 1);
memcpy(chars.get(), GetStringData(), m_size);
memcpy(chars.get() + m_size, other.GetStringData(), other.GetLength() + 1);
m_chars = move(chars);
m_size = length;
return *this;
}
bool CompareStrings(const CMyString &leftString, const CMyString &rightString)
{
size_t sizeOfLeftString = leftString.GetLength();
size_t sizeOfRightString = rightString.GetLength();
return (sizeOfLeftString == sizeOfRightString) ?
memcmp(leftString.GetStringData(), rightString.GetStringData(), sizeOfRightString) == 0 :
false;
}
Название функции выбрано неудачно. Уместее будет StringsAreEqual
Исправлено
CMyString operator +(const CMyString &leftString, const CMyString &rightString)
{
if (leftString.Empty() && rightString.Empty())
{
return "";
}
CMyString str(leftString); // Создается копия левой строки
str += rightString; // Создается итоговая строка, в которую копируется копия левой и правая.
return str;
}
Желательно реализовать без лишнего выделения: (выделить под результат, скопировать левую и правую половины). Предлагаемый вариант - приватный конструктор, принимающий unique_ptr (по rvalue-ссылке) и длину. Оператор + сделать другом
Заменено на
CMyString operator +(const CMyString &leftString, const CMyString &rightString)
{
if (leftString.Empty() && rightString.Empty())
{
return "";
}
auto temp = std::make_unique<char[]>(leftString.m_size + rightString.m_size + 1);
memcpy(temp.get(), leftString.GetStringData(), leftString.m_size);
memcpy(temp.get() + leftString.m_size, rightString.GetStringData(), rightString.m_size + 1);
return CMyString(std::move(temp), leftString.m_size + rightString.m_size);
}
При отключении SDL-проверок валятся тесты SDL добавляет доп. код, который, в частности, заполняет память, содержащуюся в объекте, нулями
BOOST_AUTO_TEST_CASE(testIndexedAccess)
{
CMyString str;
BOOST_CHECK(str[0] == '\0');
BOOST_CHECK_THROW(str[1], std::out_of_range);
CMyString hello("Hello");
BOOST_CHECK(hello[0] == 'H');
BOOST_CHECK_EQUAL(hello[5], '\0');
BOOST_CHECK_THROW(hello[6], std::out_of_range);
}
size_t CMyString::GetLength() const
{
return m_chars ? m_size : 0;
}
Это "костыль", маскирующий нарушение инвариантов объекта. Поле m_size следует держать валидным
данный конструктор может выбросить исключение (потому как make_unique использует new). если функция, помечена как noexcept или throw() выбросит исключение вызовется terminate
make_unique<char[]>('\0');
выделяет блок размером 0 байт (+служебные данные) в куче и возвращает ненулевой указатель. Фактически разыменовывать этот указатель нельзя. Если вызвать GetStringData() и передать его хоть в printf, то будет неопределенное поведение из-за разыменования этого указателяВ любом случае - это невалидное состояние строки