User 13204940 Ответов: 1

Новый для C++ - правильный подход до сих пор?


Привет,

В рамках личного проекта с Unreal Engine 4 я изучаю c++. После долгих размышлений я думаю, что наконец-то начинаю понимать некоторые новые концепции, которые вы просто не видите в управляемых языках. Мои цели здесь следующие;

1. Чтобы правильно управлять всеми памяти, чтобы избежать утечки памяти
2. чтобы весь код был как можно более компактным, но не уродливым
3. Соблюдать стандарты C++ как можно больше

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

Этот класс называется ContainerAttributes и фактически представляет собой словарь имен и значений, например capacity-100, health-75 и т. д. Поскольку разные объекты будут иметь разную информацию о хранении, например, некоторые элементы могут быть непобедимы и не иметь атрибутов здоровья, я ничего жестко не кодирую, а скорее позволяю программисту динамически указывать атрибуты и значения.

.ч:
#pragma once

#include "DebugHelper.h"

#include "CoreMinimal.h"

class NOCTURNALSYNERGY_API ContainerAttributes
{
public:
    ContainerAttributes();
    ~ContainerAttributes();
	void SetValue(string name, int value);
    int GetValue(string name);

    void Debug();

private:
    map<string, int> values;

};


Как я уже упоминал, у меня нет опыта управления памятью, но я понимаю, что все, созданное с помощью "new", возвращает указатель на объект, который должен быть удален, чтобы освободить память с помощью "delete". Насколько мне известно, ~ методы-это деконструкторы, которые возникают в следующей ситуации;

obj->containerAttributes = new ContainerAttributes();
obj->whatever();
delete obj;

(then inside obj)
obj::~obj()
{
delete containerAttributes;
}


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

.СРР:
#include "ContainerAttributes.h"

ContainerAttributes::ContainerAttributes()
{
    values = new map<string, int> {};
}

ContainerAttributes::~ContainerAttributes()
{
    delete values;
}

void ContainerAttributes::SetValue(string name, int value)
{
    auto it = values.find(name);

    if(it != values.end())
    {
        values[name] = value;
    }
    else
    {
        values.insert(pair<string, int>(name, value));
    }
}

int ContainerAttributes::GetValue(string name)
{
    auto it = values.find(name);

    if(it != values.end())
    {
        return it->second;
    }
    else
    {
        DebugHelper::Debug("Error in ContainerAttributes; map does not contain name '" + name + "'");
    }
}

void ContainerAttributes::Debug()
{
    map<string, int>::iterator it;

    for(it = values.begin(); it != values.end(); it++)
    {
        DebugHelper::Debug(it->first + ": " + it->second);
    }
}


Несколько других вопросов, касающихся этого, DebugHelper::Debug принимает FString. Можно ли вообще конкатенировать строки таким образом, используя'+', или мне нужно использовать другой метод? Я с удовольствием создам дополнительный метод для приема std::string, но я не уверен, что '+' будет работать и там.

Наконец, лучше ли его использовать
auto it = values.find(name);

или я должен явно указать что-то вроде этого?
pair<string, int> it = values.find(name);


Спасибо за любую помощь, которую вы можете дать.

Что я уже пробовал:

--------------------------------------------------------------------------

1 Ответов

Рейтинг:
1

Jochen Arndt

Если вы избегаете использования выделенных переменных-членов, то деструктор членов вызывается автоматически, что снижает вероятность утечки памяти. С вашим obj пример похоже, что вы можете сделать это, изменив член из *ContainerAttributes к ContainerAttributes.

Вы также можете подумать о том, чтобы сделать свой ContainerAttributes класс std::map основывающийся:

class NOCTURNALSYNERGY_API ContainerAttributes : public std::map<std::string, int>
{
public:
    ContainerAttributes();
    ~ContainerAttributes();
    void SetValue(const std::string& name, int value)
    {
        auto it = find(name);
        if (it != end())
            it->second = value;
        else
            insert(std::pair<std::string, int>(name, value));
    }
    int GetValue(const std::string& name) const
    {
        const auto it = find(name);
        return it == end() ? -1 : it->second;
    }
};
Однако это сделает доступ к карте общедоступным. Обратите также внимание, что я изменил name параметры const и передается по ссылке, которая здесь является подходящей передачей.

Согласно FString | Unreal Engine[^] документация, которую он предоставляет += оператор (который, вероятно, просто звонит Append() внутренне). Таким образом, вы можете использовать + оператор. Но в вашем случае
DebugHelper::Debug(it->first + ": " + it->second);
операция выполняется на (и как) std::string и не включил FString.


Когда использовать auto а когда нет-это ваше решение и обычно выбирается в зависимости от контекста. Просто поищите в интернете что-нибудь вроде "c++ auto or not". Это, наверное, хорошо читать: Не используйте <auto>, Если вы не имеете в виду это — Джозеф Мэнсфилд[^].


[no name]

Очень полезно, спасибо. Всего несколько вещей, что вы подразумеваете под изменением члена с *ContainerAttributes на ContainerAttributes?

Кроме того, является ли идея "const auto it" главным образом для отображения намерения, которое возможно, потому что оно->second не модифицируется, а только читается?

Jochen Arndt

Член вашего класса obj на самом деле является указателем (используя new). Но если у вас есть только один экземпляр (не массив), нет необходимости использовать указатель / выделенный экземпляр. Просто используйте один экземпляр в качестве члена:
class obj
{
// ...
// was:
//ContainerAttributes* containerAttributes;
// better:
ContainerAttributes containerAttributes;
};

Затем конструктор вызывается из конструктора obj. И то же самое для деструктора. Нет необходимости звонить
delete containerAttributes;
в деструкторе obj. Деструктор ContainerAttributes вызывается автоматически при удалении std::map.

const указывает компилятору не разрешать изменять объект. Он должен всегда использоваться, когда объект не изменяется.

[РЕДАКТИРОВАТЬ]
В этом частном случае на самом деле нет необходимости использовать const для итератора, потому что я сделал функцию GetValue() const. В результате компилятор автоматически выберет перегрузку find (), которая возвращает итератор const при использовании auto. Таким образом, это хороший пример преимущества использования auto (в рамках функций-членов const).
[/РЕДАКТИРОВАТЬ]

CPallini

Что о

пустота метода setValue(константные СТД::строка&амп; название, значение типа int)
{
this->operator[](name) = значение;
}

?

Jochen Arndt

Мой виртуальный 5!

[no name]

CPallini, почему вы используете const в этой ситуации для строки std::, но не для int?

CPallini

Const не имеет смысла для параметра, передаваемого по значению.

[no name]

Итак, из вашего примера я понял, что вы передаете строку std::по ссылке, которая не должна быть изменена. Почему бы вам просто не передать его по значению?

CPallini

Потому что это было бы дороже (была бы сделана свежая копия переданной строки).

Jochen Arndt

Если вы передадите его по значению, то копия должна быть создана (что включает вызов конструктора и деструктора, которые вызывают new() и delete с динамическими объектами, такими как std::string), в то время как передача ссылки (или указателя) просто передает адрес (целое число). Таким образом, использование ссылки происходит быстрее, производит меньше кода и потребляет меньше памяти.

Поскольку функция может изменить содержимое переменной, передаваемой по ссылке или указателю, const избегает этого (ошибка компиляции) и сообщает также разработчику, использующему эту функцию, что переданная переменная не изменяется.

Общее правило:
Передавайте фундаментальные типы, такие как bool, char, wchar_t, int, long, float и double по значению, а все остальные-по ссылке (предпочтительно) или указателю. Используйте const со ссылками и указателями, когда значение не изменяется.

[no name]

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

то

char*&
предназначен для обозначения массива символов, передаваемого по ссылке, используемого, например:
DebugHelper::Debug("this is a char*");


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ConversionHelper.h"
#include <string>
#include "Runtime/Engine/Classes/Engine/Engine.h"

#include "CoreMinimal.h"

class NOCTURNALSYNERGY_API DebugHelper
{
public:
	static void Debug(const FString& f);
	static void Debug(const std::string& s);
	static void Debug(const char*& c);
	static void Debug(int i);

private:
	static void _Debug(const FString& f);

};


// Fill out your copyright notice in the Description page of Project Settings.

#include "DebugHelper.h"

void DebugHelper::Debug(const FString& f)
{
	DebugHelper::_Debug(f);
}

void DebugHelper::Debug(const std::string& s)
{
	DebugHelper::_Debug(ConversionHelper::StringToFString(s));
}

void DebugHelper::Debug(const char*& c)
{
	DebugHelper::_Debug(ConversionHelper::StringToFString(c));
}

void DebugHelper::Debug(int i)
{
	DebugHelper::_Debug(ConversionHelper::IntToFString(i));
}

void DebugHelper::_Debug(const FString& f)
{
	GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, f);
	UE_LOG(LogTemp, Log, TEXT("DEBUG | %s"), *f);
}

Jochen Arndt

Это, как правило, не имеет никакого смысла, чтобы передать ссылку на указатель. В таких случаях просто передайте указатель (потому что это адрес, который является целым числом, которое является фундаментальным типом):
void DebugHelper::Debug(const char* s)

[no name]

Я пытался объявить массив char, как

const char[] s
причиной синтаксической ошибки

Jochen Arndt

Это недопустимая строка кода C/C++.
Для параметров функции можно использовать char* и char [] (они эквивалентны).

[no name]

Хорошо, это одна из тех странных вещей, которая является кривой обучения c++. Поправьте меня, если я ошибаюсь, но я понимаю, что char* - это одновременно указатель на символ и символ "массив", но на самом деле он указывает на первый символ в массиве.

В противном случае я не вижу разницы между указателем char и массивом char

Edit: я только что видел пример, и [] стоит после имени параметра, а не типа, так что

const type[] whatever
это неправильно, тогда как
const type whatever[]
правильно.

Кроме того, я бы не проходил здесь по ссылке, потому что char-это родной тип, не так ли?

Jochen Arndt

Тип не имеет значения. Массив всегда передается как указатель за кулисами.
Видишь ли https://www.cs.bu.edu/teaching/cpp/string/array-vs-ptr/

[no name]

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

const char* charArray;
или
const char*& charArray
?

Я предполагаю, что последнее точно такое же, как
const char& charArray[]

Jochen Arndt

Просто используйте char* (или массив, но это не так часто встречается).

Как уже было сказано:
Указатели являются фундаментальными типами и не нуждаются в передаче в качестве ссылки, а массивы, передаваемые в качестве параметров функций, передаются в качестве указателей.

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

Также довольно редко используется синтаксис массива для параметров функций (больше в C++, чем в C), но иногда он используется при наличии многомерного массива.

[no name]

User-2223753, конечно, если конструктор для containerAttributes вызывается автоматически, то нет никакого контроля над тем, когда он создается. В других языках ContainerAttributes containerAttributes просто создает объект, но не создает его; как вы можете управлять этим здесь?

Jochen Arndt

Существует контроль: он создается, когда создается экземпляр объекта, имеющего его в качестве члена.

В вашем случае:
При создании экземпляра obj конструктор этого класса вызывает конструкторы всех членов (если таковые имеются). Это требует, чтобы у этих членов не было конструктора или хотя бы одного без параметров.

[no name]

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

ContainerAttributes* containerAttributes;

и когда захочу,
containerAttributes = new ContainerAttributes();

Jochen Arndt

Обычно нет необходимости создавать его позже, когда имеется один экземпляр (а не массив переменного размера) или объект довольно велик.

Одной из ваших целей было избежать утечки памяти. И не используя new() здесь это делается. С указателями вы также должны убедиться, что они инициализированы с помощью nullptr (в конструкторе), и все обращения должны быть защищены проверкой допустимого указателя (по крайней мере, для отладочных сборок). Отказ от использования new() приводит также к меньшему количеству кода (как в исходном, так и в сгенерированном) и более быстрому выполнению.

Поэтому не используйте new (), когда в этом нет необходимости.