Huawei C++ Programlama Kuralları

  • Huawei C++ Programlama Kuralları

C++ Dili Programlama Kuralları

Amaç

Kurallar mükemmel değildir ve belirli durumlarda faydalı olan özellikleri yasaklayarak kod uygulamasını etkileyebilir. Ancak kuralların amacı “çoğu programcının daha fazla fayda elde etmesini sağlamak"tır. Takım çalışması sırasında bir kuralın izlenemeyeceğini düşünüyorsanız, umuyoruz ki bu kuralı birlikte geliştirebilirsiniz. Bu规范ı incelemeden önce, C++ dilini öğrenmek için temel bir yeteneğe sahip olduğunuzu varsayıyoruz, C++ dilini öğrenmek için bu belgeyi kullanmamanızı öneririz.

  1. C++ dilinin ISO standardını öğrenin;
  2. C++ 03/11/14/17 ilgili özellikler dahil olmak üzere C++ dilinin temel dilsel özelliklerine hakim olun;
  3. C++ dilinin standart kütüphanesini öğrenin;

Genel İlkeler

Kodun okunabilirlik, bakımı kolaylık, güvenli, güvenilir, test edilebilir, verimli, taşınabilir özelliklerini karşılamak için işlevselliği doğru bir şekilde korumak gerekir.

Öncelikli Odak

  1. İsimlendirme, düzen vb. gibi C++ dilinin programlama tarzı kurallarını belirlemek.
  2. Başlık dosyaları, sınıflar, arayüzler ve fonksiyonların nasıl tasarımlanacağına dair C++ dilinin modüler tasarımına yönelik kurallar.
  3. C++ dilinin sabitler, tip dönüşümleri, kaynak yönetimi, şablonlar vb. ilgili özelliklerinin iyi uygulamaları.
  4. Kodun bakımı kolaylığı ve güvenilirliğini artıran C++11/14/17’deki modern C++ dilinin iyi uygulamaları.
  5. Bu规范öncelikli olarak C++17 sürümüne uygun olmalıdır.

Kurallar

Kural: Programlama sırasında uyulması zorunlu kurallar (must)

Öneri: Programlama sırasında uyulması gereken kurallar (should)

Bu规范genel C++ standardına uygundur. Eğer belirli bir sürüm standardı belirtilmemişse, tüm sürümlere (C++03/11/14/17) uygundur.

İstisnalar

‘Kural’ veya ‘Öneri’ ne olursa olsun, bu maddeye neden böyle bir düzenlemenin yapıldığını anlamak ve buna uyum sağlamak gerekir. Ancak, bazı kurallar ve öneriler istisnalar içerebilir.

Genel ilkelerin ihlal edilmediği, derinlemesine düşünülen ve yeterli gerekçelere sahip olunan durumlarda,规范da belirtilenlere aykırı davranılabilir. İstisalar kodun tutarlılığını bozar, lütfen mümkün olduğunca kaçının. ‘Kural’ istisaları çok az olmalıdır.

Aşağıdaki durumlarda, tutarlılık ilkeleri öncelikli olmalıdır: Üçüncü taraf kodları, açık kaynak kodları değiştirirken, mevcut açık kaynak kodu ve üçüncü taraf kodun kurallarına uyulmalı ve tutarlılık korunmalıdır.

2 İsimlendirme

Genel İsimlendirme

Deve Üstü Stili (CamelCase) Büyük ve küçük harfler birlikte kullanılır, kelimeler birbirine bağlanır ve farklı kelimeler büyük harfle ayrılabilir. Birleştirildikten sonra ilk harfin büyük olup olmamasına göre, Büyük Deve Üstü (UpperCamelCase) ve Küçük Deve Üstü (lowerCamelCase) olarak ayrılır.

Tür İsimlendirme Stili
Sınıf türleri, yapı türleri, numaralandırma türleri, birleşim türleri vb. tip tanımları, kapsam adları Büyük Deve Üstü
Fonksiyonlar (küresel fonksiyonlar, kapsam fonksiyonları, üye fonksiyonlar dahil) Büyük Deve Üstü
Küresel değişkenler (küresel ve isim alanı değişkenleri, sınıf statik değişkenleri dahil), yerel değişkenler, fonksiyon parametreleri, sınıf, yapı ve birleşimlerdeki üye değişkenler Küçük Deve Üstü
Makrolar, sabitler (const), numaralandırma değerleri, goto etiketleri Tamamen büyük harf, alt çizgi ile ayrılır

Not: Yukarıdaki tabloda sabit ifadesi, küresel kapsam, isim alanı, sınıf statik üyesi kapsamındaki, const veya constexpr ile değiştirilmiş temel veri tipleri, numaralandırma, dize tipleri değişkenlerini ifade eder, diziler ve diğer tip değişkenler dahil değildir. Yukarıdaki tabloda değişken ifadesi, sabit tanımının dışında kalan diğer değişkenleri ifade eder, hepsi küçük deve üstü stilini kullanır.

Dosya İsimlendirme

Kural 2.2.1 C++ dosyaları .cpp uzantısı ile, başlık dosyaları .h uzantısı ile bitmeli

.h uzantısını başlık dosyaları için kullanmanızı öneririz, böylece başlık dosyası doğrudan C ve C++ uyumluluğuna sahip olur. .cpp uzantısını uygulama dosyaları için kullanmanızı öneririz, böylece doğrudan C++ kodunu C kodundan ayırt edebilirsiniz.

Şu anda endüstride bazı diğer uzantı gösterim yöntemleri de vardır:

  • Başlık dosyaları: .hh, .hpp, .hxx
  • cpp dosyaları: .cc, .cxx, .c

Eğer mevcut proje grubu belirli bir uzantı kullanıyorsa, bunu kullanmaya devam edebilirsiniz, ancak stil tutarlılığını korumalısınız. Ancak bu belge için, .h ve .cpp uzantılarını varsayılan olarak kullanıyoruz.

Kural 2.2.2 C++ dosya adları sınıf adlarıyla eşleşmelidir

C++ başlık dosyaları ve cpp dosya adları sınıf adlarıyla eşleşmelidir ve alt çizgi küçük harf stilini kullanmalıdır. Eğer DatabaseConnection adında bir sınıfınız varsa, ilgili dosya adları:

  • database_connection.h
  • database_connection.cpp

Yapı, isim alanı, numaralandırma vb. tanımlamalar için dosya adları da benzerdir.

Fonksiyon İsimlendirme

Fonksiyon isimlendirme büyük deve üstü stilini birleştirmek için birleştirilmiş eylem veya eylem nesnesi yapısını kullanır.

class List {
public:
	void AddElement(const Element& element);
	Element GetElement(const unsigned int index) const;
	bool IsEmpty() const;
};

namespace Utils {
    void DeleteUser();
}

Tip İsimlendirme

Tip isimlendirme büyük deve üstü isimlendirme stilini kullanır. Tüm tip isimlendirme — sınıf, yapı, birleşim, tip tanımları (typedef), numaralandırmalar — aynı kurallara uymalıdır, örneğin:

// sınıflar, yapılar ve birleşimler
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
union Packet { ...

// typedefs
typedef std::map<std::string, UrlTableProperties*> PropertiesMap;

// numaralandırmalar
enum UrlTableErrors { ...

İsim alanı isimlendirmesi için büyük deve üstü kullanımını öneririz:

// isim alanı
namespace OsUtils {
 
namespace FileUtils {
     
}
 
}

Öneri 2.4.1 typedef veya #define ile temel tiplerin takma adlarla yeniden tanımlanmasını kaçının

Açık bir gerekli olmaması durumunda, temel veri tiplerini typedef/#define ile yeniden tanımlamayın. <stdint.h> başlık dosyasındaki temel tipleri tercih edin:

İşaretli tipler İşaretsiz tipler Açıklama
int8_t uint8_t 8 bit işaretli/İşaretsiz tamsayı tipi
int16_t uint16_t 16 bit işaretli/İşaretsiz tamsayı tipi
int32_t uint32_t 32 bit işaretli/İşaretsiz tamsayı tipi
int64_t uint64_t 64 bit işaretli/İşaretsiz tamsayı tipi
intptr_t uintptr_t İşaretçi saklamak için yeterli işaretli/İşaretsiz tamsayı tipi

Değişken İsimlendirme

Genel değişken isimlendirme küçük deve üstü, küresel değişkenler, fonksiyon parametreleri, yerel değişkenler, üye değişkenler dahil.

std::string tableName;  // İyi: Bu tarz önerilir
std::string tablename;  // Kötü: Bu tarz yasak
std::string path;       // İyi: Tek kelime olduğunda, küçük deve üstü tamamen küçük harf olur

Kural 2.5.1 Küresel değişkenler ‘g_’ öneki eklenmelidir, statik değişkenlerin isimlendirmesinde özel bir önek eklenmez

Küresel değişkenler mümkün olduğunca az kullanılmalıdır ve kullanılacaksa özellikle dikkat edilmelidir, bu yüzden görsel olarak belirgin hale getirmek için önek eklenir ve bu değişkenlerin kullanımını daha dikkatli yapmaya teşvik eder.

  • Küresel statik değişkenlerin isimlendirmesi küresel değişkenlerle aynıdır.
  • Fonksiyon içindeki statik değişkenlerin isimlendirmesi normal yerel değişkenlerle aynıdır.
  • Sınıfın statik üye değişkenleri ve normal üye değişkenleri aynıdır.
int g_activeConnectCount;

void Func()
{
    static int packetCount = 0; 
    ...
}

Kural 2.5.2 Sınıfın üye değişkenleri küçük deve üstü ve alt çizgi ile isimlendirilmelidir

class Foo {
private:
    std::string fileName_;   // _ son eki eklenir, K&R isimlendirme tarzına benzer
};

struct/union’un üye değişkenleri için, küçük deve üstü son ek olmadan isimlendirme tarzı kullanılır ve yerel değişkenlerin isimlendirme tarzıyla aynıdır.

Makro, Sabit, Numaralandırma İsimlendirme

Makrolar, numaralandırma değerleri tamamen büyük harf, alt çizgiyle bağlanır. İsim alanı içindeki küresel const sabitler, isim alanı içindeki anonim const sabitler, sınıfın statik üye sabitleri tamamen büyük harf, alt çizgiyle ayrılır; fonksiyonun yerel const sabitleri ve sınıfın normal const üye değişkenleri küçük deve üstü isimlendirme stilini kullanır.

#define MAX(a, b)   (((a) < (b)) ? (b) : (a)) // Sadece makro isimlendirmesi için örnek, böyle bir işlevi makro ile gerçekleştirmeniz önerilmez

enum TintColor {    // Dikkat, numaralandırma tipi adı büyük deve üstü, altındaki değerler tamamen büyük harf, alt çizgiyle birbirine bağlanır
    RED,
    DARK_RED,
    GREEN,
    LIGHT_GREEN
};

int Func(...)
{
    const unsigned int bufferSize = 100;    // fonksiyon yerel sabiti
    char *p = new char[bufferSize];
    ...
}

namespace Utils {
	const unsigned int DEFAULT_FILE_SIZE_KB = 200;        // küresel sabit
}

3 Format

Satır Genişliği

Kural 3.1.1 Satır genişliği 120 karakterden fazla olmamalıdır

Satır karakter sayısı 120’dan fazla olmamalıdır. 120 karakterden fazla olursa, uygun bir şekilde satır sonuna geçiş yapmalısınız.

İstisna:

  • Bir satır açıklamada 120 karakterden fazla komut veya URL varsa, kopyalamak, yapıştırmak ve grep ile aramak için daha uygun olması için tek bir satırda bırakılabilir;
  • Uzun yolu içeren #include ifadeleri 120 karakterden fazla olabilir, ancak yine de mümkün olduğunca kaçınılmalıdır;
  • Derleme önişlemesindeki hata bilgileri 120 karakterden fazla olabilir. Önişlemci hatası bilgileri, 120 karakterden fazla olsa bile satırda okunması ve anlaşılması kolay olur.
#ifndef XXX_YYY_ZZZ
#error Header aaaa/bbbb/cccc/abc.h must only be included after xxxx/yyyy/zzzz/xyz.h, because xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#endif

Girinti

Kural 3.2.1 Girinti için boşluk kullanın, her girinti 4 boşluk olmalıdır

Girinti için sadece boşluk (space) kullanılabilir, her girinti 4 boşluk olmalıdır. Girinti için Tab karakteri kullanılamaz. Şu anki neredeyse tüm entegre geliştirme ortamları (IDE) Tab karakterini otomatik olarak 4 boşluğa genişletme seçeneğini destekler; IDE’nizi boşluk kullanarak girintileme desteğiyle yapılandırın.

Ayraçlar

Kural 3.3.1 K&R girinti stilini kullanın

K&R stili Satır sonuna gelindiğinde, fonksiyonların (lambda ifadeleri hariç) sol ayraçları yeni bir satırda başa ve tek başına yer alır; diğer sol ayraçlar ifadenin sonuna eklenir. Sağ ayraç tek başına bir satırda yer alır, aksi takdirde aynı ifadenin geri kalan kısmı olan do ifadesindeki while, if ifadesindeki else/else if, veya virgül, noktalı virgül gibi ifadelerle birlikte olur.

Örneğin:

struct MyType {     // İfade sonuna eklenir, önünde 1 boşluk
    ...
};

int Foo(int a)
{                   // fonksiyon sol ayraçları tek başına bir satırda, başta yer alır
    if (...) {
        ...
    } else {
        ...
    }
}

Bu tarzı tercih etme sebepleri:

  • Kod daha sıkı;
  • Satır sonuna eklemeye göre, kod okuma ritmi daha devamlı;
  • Sonraki dillerin alışkanlıklarına ve endüstrideki ana akım alışkanlıklara uygun;
  • Modern entegre geliştirme ortamları (IDE) kod girinti hizalamasını gösteren yardımcı özellikler sunar, ayraçları satır sonuna koymak girinti ve kapsam üzerindeki etkisini anlamada zorluk yaratmaz.

Boş fonksiyon gövdeleri için, ayraçları aynı satıra koyabilirsiniz:

class MyClass {
public:
    MyClass() : value_(0) {}
   
private:
    int value_;
};

Fonksiyon Bildirimi ve Tanımı

Kural 3.4.1 Fonksiyon bildirim ve tanımındaki dönüş tipi ve fonksiyon adı aynı satırda olmalıdır; fonksiyon parametre listesi satır genişliğini aştığında satır sonuna geçmeli ve uygun şekilde hizalanmalıdır

Fonksiyon bildirimi ve tanımı yapılırken fonksiyonun dönüş tipi fonksiyon adıyla aynı satırda olmalıdır; satır genişliği izin veriyorsa fonksiyon parametreleri de aynı satırda olmalıdır; aksi takdirde fonksiyon parametreleri satır sonuna geçmeli ve uygun şekilde hizalanmalıdır. Parametre listesinin sol parantezi her zaman fonksiyon adıyla aynı satırda olmalı, tek başına bir satırda olmamalı; sağ parantez her zaman son parametreyle aynı satırda olmalıdır.

Satır sonuna geçirme örneği:

ReturnType FunctionName(ArgType paramName1, ArgType paramName2)   // İyi:tümü aynı satırda
{
    ...
}

ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1,     // Satır genişliği tüm parametrelere izin vermiyor, satır sonuna geçirildi
                                        ArgType paramName2,     // İyi:üstteki parametreye hizalanmış
                                        ArgType paramName3)
{
    ...
}

ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // Satır genişliği kısıtlamaları, satır sonuna geçirildi
    ArgType paramName3, ArgType paramName4, ArgType paramName5)     // İyi: 4 boşluk girinti ile satır sonuna geçirildi
{
    ...
}

ReturnType ReallyReallyReallyReallyLongFunctionName(            // Satır genişliği 1. parametreye izin vermiyor, doğrudan satır sonuna geçirildi
    ArgType paramName1, ArgType paramName2, ArgType paramName3) // İyi: 4 boşluk girinti ile satır sonuna geçirildi
{
    ...
}

Fonksiyon Çağırma

Kural 3.5.1 Fonksiyon çağrısı giriş parametresi listesi bir satırda olmalı, satır genişliğini aştığında parametreler uygun şekilde hizalanmalı

Fonksiyon çağırılırken fonksiyon parametre listesi bir satırda olmalı. Parametre listesi satır genişliğini aşıyorsa satır sonuna geçirilmeli ve uygun şekilde parametreler hizalanmalı. Sol parantez her zaman fonksiyon adıyla aynı satırda olmalı, sağ parantez her zaman son parametreyle aynı satırda olmalı.

Satır sonuna geçirme örneği:

ReturnType result = FunctionName(paramName1, paramName2);   // İyi:fonksiyon parametreleri aynı satırda

ReturnType result = FunctionName(paramName1,
                                 paramName2,                // İyi:üstteki parametreyle hizalanmış
                                 paramName3);

ReturnType result = FunctionName(paramName1, paramName2,
    paramName3, paramName4, paramName5);                    // İyi:4 boşluk girinti ile satır sonuna geçirildi

ReturnType result = VeryVeryVeryLongFunctionName(           // Satır genişliği 1. parametreye izin vermiyor, doğrudan satır sonuna geçirildi
    paramName1, paramName2, paramName3);                    // Satır sonuna geçirildi, 4 boşluk girinti

Fonksiyon çağrısı parametreleri içsel ilişkiliyse, anlaşılma kolaylığını önceliklendirin, format düzenine göre uygun parametre gruplarını satır sonuna geçirin.

// İyi:her satırdaki parametreler güçlü ilişkili bir veri yapısı grubunu temsil eder, aynı satırda olmak daha anlaşılır
int result = DealWithStructureLikeParams(left.x, left.y,     // Bir grup ilişkili parametre gösterir
                                         right.x, right.y);  // Başka bir grup ilişkili parametre gösterir

if ifadesi

Kural 3.6.1 if ifadeleri ayraç kullanmalıdır

if ifadelerinin ayraç kullanmasını gerektiririz, tek satırlık bir ifade olsa bile.

Gerekçeler:

  • Kod mantığı açık ve okunaklı;
  • Mevcut koşul ifadesi koduna yeni kod eklendiğinde hata yapma olasılığı azalır;
  • if ifadesinde fonksiyonel makro kullanıldığında ayraç koruması hata yapma olasılığını azaltır (makro tanımı ayraçları unutursa).
if (objectIsNotExist) {         // İyi:tek satırlık koşul ifadesi de ayraç ekler
    return CreateNewObject();
}

Kural 3.6.2 if/else/else if aynı satırda yazılmamalıdır

Koşul ifadesinde birden fazla dal varsa farklı satırlara yazılmalıdır.

İşte doğru yazım örnekleri:

if (someConditions) {
    DoSomething();
    ...
} else {  // İyi: else if farklı satırda
    ...
}

İşte规范e uygun olmayan örnekler:

if (someConditions) { ... } else { ... } // Kötü: else if aynı satırda

Döngü İfadeleri

Kural 3.7.1 Döngü ifadeleri ayraç kullanmalıdır

Koşul ifadelerine benzer şekilde, for/while döngü ifadeleri ayraç kullanmalıdır, döngü ifadesi boş olsa veya sadece bir satırdan oluşsa bile.

for (int i = 0; i < someRange; i++) {   // İyi: ayraç kullanıldı
    DoSomething();
}
while (condition) { }   // İyi:döngü gövdesi boş, ayraç kullanıldı
while (condition) {
    continue;           // İyi:continue boş mantığı gösterir, ayraç kullanıldı
}

Kötü örnekler:

for (int i = 0; i < someRange; i++)
    DoSomething();      // Kötü:ayraç eklenmeli
while (condition);      // Kötü:noktalı virgül while ifadesinin bir parçasıymış gibi görünebilir

switch ifadesi

Kural 3.8.1 switch ifadesindeki case/default ayraçları bir seviye girintilenmelidir

switch ifadesinin girinti tarzı şu şekildedir:

switch (var) {
    case 0:             // İyi: girinti
        DoSomething1(); // İyi: girinti
        break;
    case 1: {           // İyi: ayraçlı format
        DoSomething2();
        break;
    }
    default:
        break;
}
switch (var) {
case 0:                 // Kötü: case girintili değil
    DoSomething();
    break;
default:                // Kötü: default girintili değil
    break;
}

İfadeler

Öneri 3.9.1 İfadeler satır sonuna geçildiğinde tutarlılık korunmalı, operatör satır sonuna konulmalı

Uzun ifadeler, satır genişliği gereksinimlerini karşılamıyorsa uygun yerlerde satır sonuna geçirilmelidir. Genellikle düşük öncelikli operatör veya bağlayıcıların ardından bölünür, operatör veya bağlayıcı satır sonuna konur. Operatör, bağlayıcı satır sonuna konur, “bitmedi, devamı var” anlamını ifade eder. Örneğin:

// Aşağıdaki ilk satırın satır genişliği gereksinimlerini karşılamadığını varsayalım

if ((currentValue > threshold) &&  // İyi:satır sonuna geçirildiğinde, mantıksal operatör satır sonuna konuldu
    someCondition) {
    DoSomething();
    ...
}

int result = reallyReallyLongVariableName1 +    // İyi
             reallyReallyLongVariableName2;

İfade satır sonuna geçirildiğinde, uygun hizalamaya dikkat edin veya 4 boşluk girinti yapın. Aşağıdaki örnekleri inceleyin

int sum = longVariableName1 + longVariableName2 + longVariableName3 +
    longVariableName4 + longVariableName5 + longVariableName6;         // İyi: 4 boşluk girinti

int sum = longVariableName1 + longVariableName2 + longVariableName3 +
          longVariableName4 + longVariableName5 + longVariableName6;   // İyi: hizalı

Değişken Atama

Kural 3.10.1 Çoklu değişken tanımı ve atama ifadeleri aynı satırda yazılmamalıdır

Her satırda sadece bir değişken başlatma ifadesi olmalıdır, böylece daha kolay okunabilir ve anlaşılabilir.

int maxCount = 10;
bool isCompleted = false;

规范e uygun olmayan örnekler aşağıda verilmiştir:

int maxCount = 10; bool isCompleted = false; // Kötü:birden fazla değişken başlatma ifadesini birden fazla satırda, her satırda bir değişken başlatma ifadesi olarak ayırmalısınız
int x, y = 0;  // Kötü:birden fazla değişken tanımı birden fazla satırda ayrılmalı, her satırda bir tanım olmalı

int pointX;
int pointY;
...
pointX = 1; pointY = 2;  // Kötü:birden fazla değişken atama ifadesi aynı satırda

İstisna: for döngü başlığı, if başlatma ifadesi (C++17), yapılandırılmış bağlama ifadesi (C++17) içinde birden fazla değişken bildirimi ve başlatma yapılabilir. Bu ifadelerdeki birden fazla değişken bildirimi güçlü ilişkili olduğundan, zorla birden fazla satıra ayırmak kapsam tutarsızlığına, bildirim ve başlatma ayrışmasına vb. sorunlara yol açar.

Başlatma

Başlatma, yapı, birleşim ve dizi başlatmalarını içerir

Kural 3.11.1 Başlatma satır sonuna geçirildiğinde girinti yapılmalı ve uygun şekilde hizalanmalı

Yapı veya dizi başlatmaları yapılırken, satır sonuna geçirildiğinde 4 boşluk girinti yapılmalıdır. Okunabilirlik açısından, satır sonuna geçirme noktası ve hizalama konumunu seçin.

const int rank[] = {
    16, 16, 16, 16, 32, 32, 32, 32,
    64, 64, 64, 64, 32, 32, 32, 32
};

İşaretçi ve Referans

Öneri 3.12.1 İşaretçi tipi “*” değişken adı veya tip ile birlikte olmalı, iki tarafta boşluk bırakılmamalı veya bırakılmamalı

İşaretçi isimlendirme: * solda ya da sağda olabilir, ancak iki tarafta da boşluk bırakılmamalı veya bırakılmamalı.

int* p = nullptr;  // İyi
int *p = nullptr;  // İyi

int*p = nullptr;   // Kötü
int * p = nullptr; // Kötü

İstisna: Değişken const ile değiştirildiğinde, “*” değişkeni takip edemez, bu durumda tipi de takip etmemelidir.

const char * const VERSION = "V100";

Öneri 3.12.2 Referans tipi “&” değişken adı veya tip ile birlikte olmalı, iki tarafta boşluk bırakılmamalı veya bırakılmamalı

Referans isimlendirme: & solda ya da sağda olabilir, ancak iki tarafta da boşluk bırakılmamalı veya bırakılmamalı.

int i = 8;

int& p = i;     // İyi
int &p = i;     // İyi
int*& rp = pi;  // İyi,işaretçinin referansı,*& tipi takip eder
int *&rp = pi;  // İyi,işaretçinin referansı,*& değişkeni takip eder
int* &rp = pi;  // İyi,işaretçinin referansı,* tipi takip eder,& değişkeni takip eder

int & p = i;    // Kötü
int&p = i;      // Kötü

Derleme önişleme

Kural 3.13.1 Derleme önişleme “#” her zaman satır başında olmalı, derleme önişleme ifadeleri iç içe olduğunda “#” girinti yapılabilir

Derleme önişleme “#” her zaman satır başında olmalı, önişleme ifadeleri fonksiyon gövdesine gömülmüş olsa bile, “#” satır başında olmalı.

Kural 3.13.2 Makroları kullanmaktan kaçının

Makrolar kapsamı, tip sistemini ve çeşitli kuralları görmezden gelir, sorunlara yol açabilir. Makro kullanımını mümkün olduğunca önlemeye çalışın, makro kullanmanız gerekirse makro adının benzersiz olduğundan emin olun. C++‘da makro kullanımının yerini alabilecek birçok yöntem vardır:

  • Anlaşılması kolay sabitleri const veya enum ile tanımlayın
  • İsim çakışmasını önlemek için namespace kullanın
  • Fonksiyon çağrısı maliyetini önlemek için inline fonksiyon kullanın
  • Birden fazla tipi işlemek için template fonksiyon kullanın

Dosya koruma makroları, koşullu derleme, günlük kaydı vb. gerekli senaryolarda makro kullanılabilir.

Kural 3.13.3 Makroları sabitleri temsil etmek için kullanmaktan kaçının

Makrolar basit metin yerine geçirme yapar, önişleme aşamasında tamamlanır, çalışma zamanı hatası olduğunda doğrudan ilgili değeri bildirir; hata ayıklama izleme sırasında da değer gösterilir, makro adı değil; makrolar tip kontrolüne sahip değildir, güvenli değildir; makrolar kapsamı yoktur.

Kural 3.13.4 Fonksiyonel makroları kullanmaktan kaçının

Makro fonksiyonel makro tanımlamadan önce, bunun yerine fonksiyon kullanılabileceğini düşünün. Değiştirilebilir senaryolarda, makro yerine fonksiyon kullanmanızı öneririz. Fonksiyonel makroların dezavantajları şunlardır:

  • Fonksiyonel makrolar tip kontrolünden yoksundur, fonksiyon çağrısından daha katı denetim sağlamaz
  • Makro genişletilirken makro parametreleri değer almaz, beklenmeyen sonuçlara yol açabilir
  • Makronun bağımsız bir kapsamı yoktur
  • Makronun teknik yönleri çok güçlüdür, örneğin # kullanımı ve her yerdeki parantezler, okunabilirliği etkiler
  • Belirli senaryolarda makronun derleyici genişletme sözdizimi, örneğin GCC’nin deyim ifadesi, taşınabilirliği etkiler
  • Makro önişleme aşamasında genişletilir ve daha sonra derleme, bağlama ve hata ayıklamada görünmez; Ayrıca çok satırlı makrolar bir satırda genişletilir. Fonksiyonel makrolar hata ayıklamada zor, kesme noktası koymada zor, sorunları bulmakta zor

Fonksiyonların makronun bu dezavantajları yoktur. Ancak, fonksiyonlar makrolara kıyasla en büyük dezavantajı yürütme verimliliğinin düşük olmasıdır (fonksiyon çağrısı maliyeti ve derleyici optimizasyon zorluğu). Bu nedenle, gerekirse iç satır fonksiyonu kullanabilirsiniz. İç satır fonksiyonu makroya benzer, çağırma noktasında genişletilir. Farkı ise iç satır fonksiyonunun derleme zamanında genişletilmesidir.

İç satır fonksiyonu fonksiyon ve makronun avantajlarını birleştirir:

  • İç satır fonksiyonları katı tip kontrolü gerçekleştirir
  • İç satır fonksiyonunun parametresi değerleri sadece bir kez alınır
  • İç satır fonksiyonu yerinde genişletilir, fonksiyon çağrısı maliyeti yoktur
  • İç satır fonksiyonu fonksiyondan daha iyi optimize edilir

Performans açısından yüksek talep duyan ürün kodu için, fonksiyon yerine iç satır fonksiyonu kullanmayı düşünebilirsiniz.

İstisna: Günlük kaydı senaryolarında, dosya adı (FILE), satır numarası (LINE) vb. gibi çağırma noktasındaki bilgileri korumak için fonksiyonel makro kullanılması gerekir.

Boşluk ve Satır Sonu

Kural 3.14.1 Yatay boşluk, anahtar kelimeleri ve önemli bilgileri vurgulamalı, gereksiz boşluğu önlemelidir

Yatay boşluk, anahtar kelimeleri ve önemli bilgileri vurgulamalı, her satırın sonunda boşluk bırakılmamalıdır. Genel kurallar aşağıdaki gibidir:

  • if, switch, case, do, while, for vb. anahtar kelimelerden sonra boşluk ekleyin;
  • Küçük parantezlerin iç kısmının her iki tarafına boşluk eklemeyin;
  • Büyük parantezlerin iç kısmının her iki tarafına boşluk ekleyin veya eklemeyin, her iki tarafın tutarlı olması gerekir;
  • Birli operatörler (& * + ‐ ~ !) ardından boşluk eklemeyin;
  • İkili operatörler (= + ‐ < > * / % | & ^ <= >= == != ) her iki tarafına boşluk ekleyin
  • Üçlü operatör (? :) sembollerinin her iki tarafına boşluk ekleyin
  • Önek ve sonek artı/eksi operatörleri (++ –) ve değişken arasında boşluk eklemeyin
  • Yapı üyesi operatörleri (. ->) öncesi ve sonrası boşluk eklemeyin
  • Virgül(,) öncesi boşluk eklemeyin, sonrası boşluk ekleyin
  • Şablonlar ve tip dönüşümü (<>) ve tipler arasında boşluk eklemeyin
  • Kapsam operatörü (::) öncesi ve sonrası boşluk eklemeyin
  • İki nokta (:), duruma göre öncesi ve sonrası boşluk ekleyin

Normal durum:

void Foo(int b) {  // İyi:büyük parantez öncesi boşluk bırakılmalı

int i = 0;  // İyi:değişken başlatmada,= her iki tarafına boşluk eklenmeli,noktalı virgül öncesi boşluk bırakılmamalı

int buf[BUF_SIZE] = {0};    // İyi:büyük parantez içi her iki tarafı boşluk bırakmaz

Fonksiyon tanımı ve fonksiyon çağrısı:

int result = Foo(arg1,arg2);
                    ^    // Kötü: virgül sonrası boşluk eklenmeli

int result = Foo( arg1, arg2 );
                 ^          ^  // Kötü: fonksiyon parametre listesinde sol parantez sonrası boşluk eklenmemeli,sağ parantez öncesi boşluk eklenmemeli

İşaretçi ve adres alma

x = *p;     // İyi:* operatörü ve işaretçi p arasında boşluk bırakılmaz
p = &x;     // İyi:& operatörü ve değişken x arasında boşluk bırakılmaz
x = r.y;    // İyi:. ile üye değişkene erişirken boşluk bırakılmaz
x = r->y;   // İyi:-> ile üye değişkene erişirken boşluk bırakılmaz

Operatörler:

x = 0;   // İyi:atama operatörü = her iki tarafına boşluk eklenir
x = -5;  // İyi:negatif sayının işareti ve değeri arasında boşluk bırakılmaz
++x;     // İyi:ön ek ve sonek ++/-- ve değişken arasında boşluk bırakılmaz
x--;

if (x && !y)  // İyi:mantıksal operatörler her iki tarafına boşluk eklenir,! operatör ve değişken arasında boşluk bırakılmaz
v = w * x + y / z;  // İyi:ikili operatörler her iki tarafına boşluk eklenir
v = w * (x + z);    // İyi:parantez içindeki ifade her iki tarafına boşluk eklenmez

int a = (x < y) ? x : y;  // İyi: üçlü operatör, ? ve : her iki tarafına boşluk eklenir

Döngü ve koşul ifadeleri:

if (condition) {  // İyi:if anahtar kelimesi ve parantez arasında boşluk eklenir,parantez içindeki koşul ifadesi her iki tarafına boşluk eklenmez
    ...
} else {           // İyi:else anahtar kelimesi ve büyük parantez arasında boşluk eklenir
    ...
}

while (condition) {}   // İyi:while anahtar kelimesi ve parantez arasında boşluk eklenir,parantez içindeki koşul ifadesi her iki tarafına boşluk eklenmez

for (int i = 0; i < someRange; ++i) {  // İyi:for anahtar kelimesi ve parantez arasında boşluk eklenir,noktalı virgül sonrası boşluk eklenir
    ...
}

switch (condition) {  // İyi: switch anahtar kelimesinden sonra 1 boşluk bırakılır
    case 0:     // İyi:case ifadesi koşulu ve iki nokta arasında boşluk bırakılmaz
        ...
        break;
    ...
    default:
        ...
        break;
}

Şablonlar ve dönüşüm

// Açılı parantezler(< and >) boşlukla bitişik olmaz, < öncesi boşluk bırakılmaz, > ve ( arasında da boşluk bırakılmaz.
vector<string> x;
y = static_cast<char*>(x);

// Tip ve işaretçi operatörleri arasında boşluk bırakılabilir, ancak tutarlı olunmalıdır.
vector<char *> x;

Kapsam operatörü

std::cout;    // İyi: isim alanı erişimi boşluk bırakmaz

int MyClass::GetValue() const {}  // İyi: Üye fonksiyon tanımı boşluk bırakmaz

İki nokta

// Boşluk bırakılacak senaryolar

// İyi: Sınıf türetmesinde boşluk bırakılmalı
class Sub : public Base {
   
};

// Oluşturma fonksiyonu başlatma listesi boşluk bırakılmalı
MyClass::MyClass(int var) : someVar_(var)
{
    DoSomething();
}

// Bit alanı gösterimi boşluk bırakmalı
struct XX {
    char a : 4;    
    char b : 5;    
    char c : 4;
};
// Boşluk bırakılmayacak senaryolar

// İyi: public:, private: gibi sınıf erişim hakları için iki nokta boşluk bırakmaz
class MyClass {
public:
    MyClass(int var);
private:
    int someVar_;
};

// switch-case için case ve default iki noktaları boşluk bırakmaz
switch (value)
{
    case 1:
        DoSomething();
        break;
    default:
        break;
}

Not: Mevcut entegre geliştirme ortamı (IDE) satır sonundaki boşlukları silmek için ayarlanabilir, lütfen doğru şekilde yapılandırın.

Öneri 3.14.1 Boş satırları uygun şekilde düzenleyin, kodu sıkı tutun

Gereksiz boş satırları azaltarak daha fazla kod gösterilebilir ve kod okunabilirliği artırılabilir. İşte uyulması önerilen bazı kurallar:

  • İçeriğin ilgili derecesine göre boş satırları uygun şekilde düzenleyin;
  • Fonksiyon içi, tip tanımı içi, makro içi, başlatma ifadesi içi ardışık boş satır kullanmayın
  • Ardışık 3 boş satır veya daha fazlasını kullanmayın, büyük parantez içindeki kod blokları için satır başı öncesi ve satır sonu sonrası boş satır eklemeyin, ancak namespace için büyük parantez içi istisna değildir.
int Foo()
{
    ...
}



int Bar()  // Kötü:en fazla ardışık 2 boş satır kullanın.
{
    ...
}


if (...) {
        // Kötü:büyük parantez içindeki kod bloğunun satır başı öncesi boş satır eklenmez
    ...
        // Kötü:büyük parantez içindeki kod bloğunun satır sonu sonrası boş satır eklenmez
}

int Foo(...)
{
        // Kötü:fonksiyon gövdesinde satır başı boş satır eklenmez
    ...
}

Sınıf

Kural 3.15.1 Sınıf erişim kontrol bloklarının bildirim sırası public:, protected:, private: olmalı, girinti class anahtar kelimesiyle aynı olmalı

class MyClass : public BaseClass {
public:      // Not boşluk bırakılmaz
    MyClass();  // Standart 4 boşluk girinti
    explicit MyClass(int var);
    ~MyClass() {}

    void SomeFunction();
    void SomeFunctionThatDoesNothing()
    {
    }

    void SetVar(int var) { someVar_ = var; }
    int GetVar() const { return someVar_; }

private:
    bool SomeInternalFunction();

    int someVar_;
    int someOtherVar_;
};

Her bölümde, benzer bildirimleri bir araya toplamayı ve aşağıdaki sıraya uygun olarak düzenlemeyi öneririz: Türler (typedef, using ve iç içe yapı ve sınıf dahil), sabitler, fabrika fonksiyonları, yapıcılar, atama operatörleri, yıkıcılar, diğer üye fonksiyonları, veri üyelerini.

Kural 3.15.2 Oluşturma fonksiyonu başlatma listesi aynı satırda veya 4 boşluk girintili çok satırda olmalı

// Eğer tüm değişkenler aynı satırda olabiliyorsa:
MyClass::MyClass(int var) : someVar_(var)
{
    DoSomething();
}

// Eğer aynı satırda olamıyorsa,
// İki nokta sonrası olmalı ve 4 boşluk girinti yapılmalı
MyClass::MyClass(int var)
    : someVar_(var), someOtherVar_(var + 1)  // İyi: virgülden sonra boşluk bırakıldı
{
    DoSomething();
}

// Eğer başlatma listesi çok satırda olmalıysa, her satırı hizalayın
MyClass::MyClass(int var)
    : someVar_(var),             // 4 boşluk girinti
      someOtherVar_(var + 1)
{ 
    DoSomething();
}

4 Açıklama

Genel olarak, kodun okunabilirliğini artırmak için temiz mimari mantığı ve iyi sembol isimlendirmeyi tercih edin; Gerektiğinde, açıklama ile destekleyin. Açıklamalar, okuyucunun kodu hızlı bir şekilde anlamasına yardımcı olmak için tasarlanmıştır, bu yüzden açıklamaya ihtiyacınız olduğunda açıklama ekleyin.

Açıklama içeriği net, anlaşılır ve tek anlamlı olmalı, bilgi eksiksiz ve tekrar içermemelidir.

Açıklamalar kod kadar önemlidir. Açıklama yazarken, okuyucunun yerine geçin ve açıklama, okuyucunun gerçekten ihtiyaç duyduğu bilgiyi ifade etmeli. Açıklama, kodun zor ifade ettiği niyeti açıklamalı, kod bilgisini tekrar etmemeli. Kodu değiştirdiğinizde, ilgili açıklamaların tutarlılığını da korumalısınız. Sadece kodu değiştirmek ve açıklamayı değiştirmemek kibar bir davranış değildir, kod ve açıklama arasındaki tutarlılığı bozar, okuyucuyu kafa karışıklığına, anlamazlığa ve yanlış anlama sürükler.

Açıklamalar için İngilizce kullanın.

Açıklama Stili

C++ kodunda /* */ ve // kullanmak mümkündür. Açıklamanın amacına ve konumuna göre, açıklama farklı tiplere ayrılabilir, örneğin dosya başı açıklaması, fonksiyon başı açıklaması, kod açıklaması vb.; Aynı tipteki açıklamalar aynı tarzda olmalıdır.

Not: Bu belgedeki örnek kodda, sorunu daha doğru bir şekilde açıklamak için büyük ölçüde ‘//’ sonrası açıklama kullanılmıştır ve bu açıklama tarzının daha iyi olduğu anlamına gelmez.

Dosya Başlığı Açıklamaları

Kural 3.1 Dosya başlığı açıklamaları telif hakkı lisansını içermelidir

/*

  • Copyright (c) 2020 XXX
  • Licensed under the Apache License, Version 2.0 (the “License”);
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an “AS IS” BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License. */

Fonksiyon Başlığı Açıklamaları

Kural 4.3.1 Herkese açık (public) fonksiyonlar fonksiyon başlığı açıklaması yazılmalıdır

Herkese açık fonksiyonlar sınıfın dışa dönük arayüzüdür, çağırıcılar fonksiyonun işlevi, parametrelerin değer aralığı, dönüş sonucu, dikkat edilmesi gerekenler vb. bilgileri anlamak için bu açıklamalara ihtiyaç duyar. Özellikle parametrelerin değer aralığı, dönüş sonucu, dikkat edilmesi gerekenler vb. bilgiler otomatik olarak ifade edilemez, bu yüzden fonksiyon başlığı açıklaması ile desteklenmelidir.

Kural 4.3.2 Boş fonksiyon başlığı açıklamaları yasaktır

Tüm fonksiyonların fonksiyon başlığı açıklamasına ihtiyacı yoktur; Fonksiyon imzası ifade edemediği bilgiler, fonksiyon başlığı açıklaması ile desteklenmelidir;

Fonksiyon başlığı açıklaması fonksiyon bildirimi veya tanımı üzerinde yer almalı, aşağıdaki tarzlardan birini kullanmalıdır: // ile fonksiyon başlığı

// Tek satırlık fonksiyon başlığı
int Func1(void);

// Çok satırlık fonksiyon başlığı
// İkinci satır
int Func2(void);

/* */ ile fonksiyon başlığı

/* Tek satırlık fonksiyon başlığı */
int Func1(void);

/*
 * Başka bir tek satırlık fonksiyon başlığı
 */
int Func2(void);

/*
 * Çok satırlık fonksiyon başlığı
 * İkinci satır
 */
int Func3(void);

Fonksiyonlar mümkün olduğunca fonksiyon adı ile kendini açıklama yapmalı, gerekli olduğunda fonksiyon başlığı açıklaması eklenebilir. Gereksiz, bilgi tekrarı yapan fonksiyon başlığı açıklamaları yazmayın; boş fonksiyon başlığı açıklamaları yazmayın.

Fonksiyon başlığı açıklaması içeriği isteğe bağlıdır, ancak sınırlı değildir: işlev, dönüş değeri, performans kısıtlamaları, kullanım, bellek anlaşmaları, algoritma uygulaması, yeniden giriş gereksinimleri vb. Modülün dış başlık dosyasındaki fonksiyon arayüzü bildirimlerinin fonksiyon başlığı açıklamaları, önemli ve faydalı bilgileri açıkça ifade etmelidir.

Örneğin:

/*
 * Gerçekten yazılan bayt sayısını döndürür, -1 yazma başarısız olduğu anlamına gelir
 * Not, bellek buf çağırıcı tarafından serbest bırakılır
 */
int WriteString(const char *buf, int len);

Kötü örnek:

/*
 * Fonksiyon adı:WriteString
 * İşlev:dizeyi yazdır
 * Parametreler:
 * Dönüş değeri:
 */
int WriteString(const char *buf, int len);

Yukarıdaki örneklerdeki sorunlar:

  • Parametre, dönüş değeri, formatı boş, içerik yok
  • Fonksiyon adı bilgisi tekrar edilir
  • Kritik buf’un kim tarafından serbest bırakıldığı açık değildir

Kod Açıklamaları

Kural 4.4.1 Kod açıklamaları ilgili kodun üstünde veya sağında olmalıdır

Kural 4.4.2 Açıklama işareti ile açıklama içeriği arasında 1 boşluk bırakılmalı; sağa konumlandırılmış açıklama ile önceki kod arasında en az 1 boşluk bırakılmalı

Kodun üzerindeki açıklama, ilgili kod ile aynı girintiye sahip olmalıdır. Aşağıdaki tarzlardan birini seçin ve birleştirin:

// kullanın


// Bu tek satırlık bir açıklamadır
DoSomething();

// Bu çok satırlı bir açıklamadır
// İkinci satır
DoSomething();

/*' '*/ kullanın

/* Bu tek satırlık bir açıklamadır */
DoSomething();

/*
 * Başka bir çok satırlı açıklama tarzı
 * İkinci satır
 */
DoSomething();

Kodun sağ tarafındaki açıklama, kod ile açıklama arasında en az 1 boşluk bırakılmalı, önerilen boşluk miktarı 4’ten fazla olmamalıdır. Genellikle genişletilmiş TAB tuşu kullanılarak 1-4 boşluk girintisi elde edilir. Aşağıdaki tarzlardan birini seçin ve birleştirin:

int foo = 100;  // Sağdaki açıklama
int bar = 200;  /* Sağdaki açıklama */

Uygun olduğunda sağa konumlandırılmış açıklama, alt alta hizalandığında daha estetik görünebilir. Hizalanmış açıklamalarda, en yakın açıklama satırı sol taraftaki koda en az 1-4 boşluk bırakmalıdır. Örneğin:

const int A_CONST = 100;         /* İlgili açıklama, alt alta hizalanabilir */
const int ANOTHER_CONST = 200;   /* Alt alta hizalandığında, sol taraftaki koda boşluk bırakılmalıdır */

Sağa konumlandırılmış açıklama satır genişliğini aştığında, açıklamayı kodun üzerine koymayı düşünmelisiniz.

Kural 4.4.3 Kullanılmayan kod bloklarını silin, açıklama yapmayın

Açıklamaya alınan kod, normal olarak bakım yapılamaz; bu kodu tekrar kullanmak istediğinde, ihmal edilemez hatalara yol açma ihtimali çok yüksektir. Doğru yaklaşım, artık gerekli olmayan kodu doğrudan silmektir. Eğer tekrar ihtiyaç duyulursa, bu kodun bir kopyasını almayı veya yeniden yazmayı düşünmelisiniz.

Burada açıklamaya alınan koddan bahsederken, /* */ ve // kullanımının yanı sıra #if 0, #ifdef NEVER_DEFINED vb. kullanımlardan da bahsedilir.

5 Başlık Dosyaları

Başlık Dosyası Sorumlulukları

Başlık dosyaları modülün veya dosyanın dış arayüzüdür, başlık dosyalarının tasarımı sistemin büyük bir kısmını tasarlama sürecini yansıtır. Başlık dosyaları, arayüz bildirimlerini koymak için uygun, ama uygulamayı (inline fonksiyonlar hariç) koymak için uygun değildir. Cpp dosyasının içindeki fonksiyonlar, makrolar, numaralandırmalar, yapı tanımları vb. başlık dosyasına konulmamalıdır. Başlık dosyası sorumlulukları tek olmalıdır. Başlık dosyası karmaşık olduğunda, bağımlılıklar da karmaşık olur ve derleme süresinin uzun olmasının ana sebeplerinden biri olur.

Öneri 5.1.1 Her .cpp dosyası için dışarıya açık hale getirilmesi gereken sınıf ve arayüzleri bildirmek amacıyla bir .h dosyası olmalıdır

Genellikle, her .cpp dosyası için karşılık gelen bir .h dosyası vardır ve dışarıya açık hale getirilmesi gereken fonksiyon bildirimlerini, makro tanımlarını, tip tanımlarını vb. koymak için kullanılır. Eğer bir .cpp dosyası dışarıya herhangi bir arayüz sağlamıyorsa, mevcut olmamalıdır. İstisna: Program giriş noktası (ör. main fonksiyonunun bulunduğu dosya), birim test kodları, dinamik kütüphane kodları.

Örnek:

// Foo.h

#ifndef FOO_H
#define FOO_H

class Foo {
public:
    Foo();
    void Fun();
   
private:
    int value_;
};

#endif
// Foo.cpp
#include "Foo.h"

namespace { // İyi: dahili fonksiyonların bildirimleri .cpp dosyasının başına konulmalı ve anonim namespace veya static olarak sınırlı olmalı
    void Bar()
    {
    }
}

...

void Foo::Fun()
{
    Bar();
}

Başlık Dosyası Bağımlılıkları

Kural 5.2.1 Başlık dosyası döngüsel bağımlılıklarını yasaklayın

Başlık dosyası döngüsel bağımlılıkları, a.h’nin b.h’yi, b.h’nin c.h’yi, c.h’nin a.h’yi içermesi anlamına gelir ve bunun sonucunda başlık dosyasındaki herhangi bir değişiklik, a.h/b.h/c.h içeren tüm kodların yeniden derlenmesine neden olur. Tek yönlü bir bağımlılık durumunda, örneğin a.h’nin b.h’yi, b.h’nin c.h’yi içermesi, c.h’nin ise herhangi bir başlık dosyası içermemesi durumunda, a.h’deki bir değişiklik b.h/c.h içeren kodların yeniden derlenmesine neden olmaz.

Başlık dosyası döngüsel bağımlılıkları, mimari tasarımın uygun olmamasını doğrudan yansıtır, mimariyi optimize ederek bunu önleyebilirsiniz.

Kural 5.2.2 Başlık dosyaları #define koruması eklenmeli, tekrarlı dahil etmeden korunmalıdır

Başlık dosyalarının tekrarlı dahil edilmesini önlemek için tüm başlık dosyaları #define koruması eklenmelidir; #pragma once kullanmaktan kaçının.

#define koruması eklerken aşağıdaki kurallara uyun:

  1. Korumalı isim benzersiz olmalıdır;
  2. Korumalı bölümün öncesinde ve sonrasına kod veya açıklama koymayın, dosya başı açıklaması istisna.

Örnek: Timer modülünün timer.h dosyası, timer/include/timer.h konumunda, aşağıdaki gibi korunmalıdır:

#ifndef TIMER_INCLUDE_TIMER_H
#define TIMER_INCLUDE_TIMER_H
...
#endif

Kural 5.2.3 Dış fonksiyon arayüzlerini, değişkenleri bildirim yoluyla kullanmaktan kaçının

Diğer modüllerin veya dosyaların sunduğu arayüzleri kullanmak için yalnızca başlık dosyası dahil etme yolu kullanılabilir. extern bildirim yoluyla dış fonksiyon arayüzlerini, değişkenleri kullanmak, dış arayüz değiştiğinde bildirim ve tanım tutarsızlığına yol açabilir. Aynı zamanda bu gizli bağımlılık, mimarinin bozulmasına yol açabilir.

规范e uygun olmayan örnek:

// a.cpp içeriği

extern int Fun();   // Kötü: extern yoluyla dış fonksiyonu kullanma

void Bar()
{
    int i = Fun();
    ...
}

// b.cpp içeriği

int Fun()
{
    // Bir şeyler yap
}

Bunun yerine aşağıdaki gibi olmalıdır:

// a.cpp içeriği

#include "b.h"   // İyi: Diğer .cpp'nin sunduğu arayüzü dahil etme yoluyla kullanma

void Bar()
{
    int i = Fun();
    ...
}

// b.h içeriği

int Fun();

// b.cpp içeriği

int Fun()
{
    // Bir şeyler yap
}

İstisna: Belirli senaryolarda dahili fonksiyonlara başvurmak gerekir, ancak kodu istila etmek istemiyorsanız extern bildirim yoluyla başvurabilirsiniz. Örneğin: Belirli bir dahili fonksiyonu birim test etmek gerektiğinde, extern bildirim yoluyla fonksiyonu başvurabilirsiniz; Belirli bir fonksiyona dikiş, yama vb. uygulama gerektiğinde, fonksiyonun extern bildirimini yapmaya izin verilir.

Kural 5.2.4 extern “C” içinde başlık dosyaları eklemek yasaktır

extern “C” içinde başlık dosyası eklemek, extern “C” iç içe geçmiş olabilir, bazı derleyicilerin extern “C” iç içe geçmiş seviyelerine sınırlamaları vardır, çok fazla iç içe geçmiş olduğunda derleme hatası alabilirsiniz.

C ve C++ karma programlama durumunda, extern “C” içinde başlık dosyası eklemek, dahili başlık dosyasının orijinal amacının bozulmasına yol açabilir, örneğin bağlantı standardının yanlış değiştirilmesi.

Örnek, a.h ve b.h iki başlık dosyası vardır:

// a.h içeriği

...
#ifdef __cplusplus
void Foo(int);
#define A(value) Foo(value)
#else
void A(int)
#endif

// b.h içeriği

...
#ifdef __cplusplus
extern "C" {
#endif

#include "a.h"
void B();

#ifdef __cplusplus
}
#endif

b.h’yi C++ önişlemcisiyle açarsanız, aşağıdaki gibi olur

extern "C" {
    void Foo(int);
    void B();
}

a.h’nin yazarının orijinal amacına göre, fonksiyon Foo bir C++ serbest fonksiyondur ve bağlantı standardı “C++” şeklindedir. Ancak b.h’de, #include "a.h" extern "C" içine konulduğundan, fonksiyon Foo’nun bağlantı standardı yanlış bir şekilde değiştirildi.

İstisna: C++ derleme ortamında, tamamen C olan başlık dosyalarını referans almak istediğinizde, bu C başlık dosyalarının extern "C" ile değiştirilmediği görülür. İstila olmayan yaklaşım, extern "C" içinde C başlık dosyası eklemektir.

Öneri 5.2.1 Başlık dosyası eklemek yerine mümkün olduğunca ön bildirimi kullanmaktan kaçının

Ön bildirim (forward declaration), genellikle sınıf, şablonun saf bildirimi, tanımıyla birlikte gelmez.

  • Avantajlar:
    1. Ön bildirim derleme süresini kazandırabilir, gereğinden fazla #include derleyiciyi daha fazla dosya açmaya ve daha fazla girdi işlemeye zorlar.
    2. Ön bildirim gerekli olmayan tekrar derleme süresini kazandırabilir. #include başlık dosyasındaki ilgisiz değişikliklerin kullanıcı kodunu tekrar derlenmesi gereken durumların atlanmasını sağlar.
  • Dezavantajlar:
    1. Ön bildirim bağımlılık ilişkilerini gizler, başlık dosyası değiştiğinde, kullanıcının kodu gerekli tekrar derleme sürecini atlayabilir.
    2. Ön bildirim kütüphanenin ilerideki değişikliklerine zarar verebilir. Ön bildirim şablonları bazen başlık dosyası geliştiricisinin API’sini değiştirmesini engeller. Örneğin parametre tipini genişletmek, varsayılan parametreli şablon parametresi eklemek vb.
    3. std:: isim alanından sembollere yönelik ön bildirimlerin davranışı tanımsızdır (C++11 standardı tarafından açıkça belirtilmiştir).
    4. Başlık dosyasından birçok sembolü ön bildirim yapmak, sadece bir satır #include’dan daha uzun olabilir.
    5. Ön bildirim yapmak için kodu yeniden düzenlemek (örneğin, nesne üyelerinin yerine işaretçi üyeler kullanmak) daha yavaş ve karmaşık bir kod yapar.
    6. Ön bildirim ve #include ne zaman kullanılacağı konusunda karar vermek zordur, bazı senaryolarda ön bildirim ve #include değiştirildiğinde beklenmeyen sonuçlar ortaya çıkabilir.

Bu yüzden mümkün olduğunca ön bildirim kullanmaktan kaçınmalı, başlık dosyası ekleyerek bağımlılıkları sağlamalıyız.

6 Kapsam

İsim Alanı

Öneri 6.1.1 Cpp dosyasında dışa aktarılmayan değişkenler, sabitler veya fonksiyonlar için anonim namespace kullanın veya static ile işaretleyin

C++ 2003 standardında, dosya kapsamındaki değişkenler, fonksiyonlar vb. için static kullanmak deprecated olarak işaretlenmiştir, bu yüzden anonim namespace kullanmak daha önerilir.

Ana sebepler:

  1. static C++‘da çok fazla anlamı vardır, statik fonksiyon üyeleri, statik fonksiyon değişkenleri, statik global değişkenler, statik fonksiyon yerel değişkenleri vb., her biri özel bir işleme sahiptir.
  2. static sadece değişkenlerin, sabitlerin ve fonksiyonların dosya kapsamını garanti eder, ancak namespace türleri de kapsayabilir.
  3. C++‘ın kapsamını yönetmek için static ve namespace’i birlikte kullanmak yerine sadece namespace’i kullanarak yönetmek daha uygun.
  4. static ile işaretsiz fonksiyonlar şablonları somutlaştırmak için kullanılamaz, anonim namespace ise kullanılabilir.

Ancak .h dosyalarında veya #include’dan önce anonim namespace veya static kullanmayın.

// Foo.cpp

namespace {
    const int MAX_COUNT = 20;
    void InternalFun() {};
}

void Foo::Fun()
{
    int i = MAX_COUNT;
   
    InternalFun();
}

Kural 6.1.1 Başlık dosyalarında veya #include öncesinde using ile isim alanı içeriğini içeri aktarmayın

Açıklama: using ile isim alanı içeriğini içeri aktarmak sonraki kodları etkiler, kolayca sembol çakışmasına yol açar, bu yüzden başlık dosyalarında ve #include öncesinde using ile isim alanı içeriğini içeri aktarmayın. Örnek:

// Başlık dosyası a.h
namespace NamespaceA {
    int Fun(int);
}
// Başlık dosyası b.h
namespace NamespaceB {
    int Fun(int);
}

using namespace NamespaceB;

void G()
{
    Fun(1);
}
// Kaynak kodu a.cpp
#include "a.h"
using namespace NamespaceA;
#include "b.h"

void main()
{
    G(); // using namespace NamespaceA #include “b.h” öncesinde, NamespaceA::Fun, NamespaceB::Fun çağırma belirsizliği yaratır
}

Başlık dosyalarında using ile tek sembol içeri aktarma veya takma ad tanımlama için, modül özel isim alanında kullanılmasına izin verilir, ancak global isim alanında kullanılması yasaktır.

// foo.h

#include <fancy/string>
using fancy::string;  // Kötü, global isim alanına sembol içeri aktarma yasaktır

namespace Foo {
    using fancy::string;  // İyi, modül özel isim alanında sembol içeri aktarma izin verilir
    using MyVector = fancy::vector<int>;  // İyi, C++11'de özel isim alanında takma ad tanımlama izin verilir
}

Küresel Fonksiyonlar ve Statik Üye Fonksiyonlar

Öneri 6.2.1 Küresel fonksiyonları yönetmek için isim alanı kullanımını tercih edin, belirli bir sınıf ile doğrudan ilişkisi olanlar için statik üye fonksiyonları kullanabilirsiniz

Açıklama: Üye olmayan fonksiyonlar isim alanı içinde yer alarak küresel kapsamı kirletmeyi önler, ayrıca isim alanı yerine sınıf + statik üye fonksiyonu ile basitçe küresel fonksiyonları yönetmeyin. Eğer bir küresel fonksiyon belirli bir sınıf ile sıkı bir ilişkiye sahipse, sınıfın statik üye fonksiyonu olarak kullanılabilir.

namespace MyNamespace {
    int Add(int a, int b);
}

class File {
public:
    static File CreateTempFile(const std::string& fileName);
};

Küresel Sabitler ve Statik Üye Sabitler

Öneri 6.3.1 Küresel sabitleri yönetmek için isim alanı kullanımını tercih edin, belirli bir sınıf ile doğrudan ilişkisi olanlar için statik üye sabitleri kullanabilirsiniz

Açıklama: Küresel sabitler isim alanı içinde yer alarak küresel kapsamı kirletmeyi önler, ayrıca isim alanı yerine sınıf + statik üye sabiti ile basitçe küresel sabitleri yönetmeyin. Eğer bir küresel sabit belirli bir sınıf ile sıkı bir ilişkiye sahipse, sınıfın statik üye sabiti olarak kullanılabilir.

namespace MyNamespace {
    const int MAX_SIZE = 100;
}

class File {
public:
    static const std::string SEPARATOR;
};

Küresel Değişkenler

Öneri 6.4.1 Mümkün olduğunca küresel değişkenlerden kaçının, Singleton tasarım kalıbını düşünün

Açıklama: Küresel değişkenler değiştirilebilir ve okunabilir, bu da iş kodu ile bu küresel değişken arasında veri bağımlılığına yol açar.

int g_counter = 0;

// a.cpp
g_counter++;

// b.cpp
g_counter++;

// c.cpp
cout << g_counter << endl;

Singleton kalıbı kullanın

class Counter {
public:
    static Counter& GetInstance()
    {
        static Counter counter;
        return counter;
    }  // Basit singleton örneği
   
    void Increase()
    {
        value_++;
    }
   
    void Print() const
    {
        std::cout << value_ << std::endl;
    }

private:
    Counter() : value_(0) {}

private:
    int value_;
};

// a.cpp
Counter::GetInstance().Increase();

// b.cpp
Counter::GetInstance().Increase();

// c.cpp
Counter::GetInstance().Print();

Singleton kalıbını uyguladıktan sonra, küresel tek bir örnek elde edilir, küresel değişkenlerle aynı etki elde edilir ve singleton daha iyi bir kapsülleme sağlar.

İstisna: Bazen küresel değişkenlerin kapsamı sadece modül içindedir, böylece süreç içinde birden fazla küresel değişken örneği olur, her modülün bir kopyası vardır, bu senaryoda singleton kalıbı ile çözülemeyen bir durumdur.

7 Sınıf

Kurucu, Kopya Kurucu, Atama ve Yıkıcı Fonksiyonlar

Kurucu, kopyalama, taşıma ve yıkıcı fonksiyonlar nesnenin yaşam döngüsü yönetimini sağlar:

  • Kurucu fonksiyon (constructor): X()
  • Kopya kurucu fonksiyon (copy constructor): X(const X&)
  • Kopya atama operatörü (copy assignment): operator=(const X&)
  • Taşıma kurucu fonksiyon (move constructor): X(X&&) C++11’den sonra
  • Taşıma atama operatörü (move assignment): operator=(X&&) C++11’den sonra
  • Yıkıcı fonksiyon (destructor): ~X()

Kural 7.1.1 Sınıfın üye değişkenleri açıkça başlatılmalıdır

Açıklama: Eğer sınıfın üye değişkenleri varsa, kurucu fonksiyon tanımlanmamışsa ve varsayılan kurucu fonksiyon tanımlanmamışsa, derleyici otomatik olarak bir kurucu fonksiyon oluşturur, ancak derleyicinin oluşturduğu kurucu fonksiyon üye değişkenleri başlatmaz, nesne durumu belirsizdir.

İstisna:

  • Eğer sınıfın üye değişkenleri varsayılan kurucu fonksiyona sahipse, açık başlatmaya gerek yoktur.

Örnek: Aşağıdaki kodda kurucu fonksiyon yoktur, özel veri üyeleri başlatılmamıştır:

class Message {
public:
    void ProcessOutMsg()
    {
        //…
    }

private:
    unsigned int msgID_;
    unsigned int msgLength_;
    unsigned char* msgBuffer_;
    std::string someIdentifier_;
};

Message message;   // message üye değişkenleri başlatılmamış
message.ProcessOutMsg();   // Sonradan kullanım riskli

// Bu yüzden, varsayılan kurucu fonksiyon tanımlamak gerekir, aşağıdaki gibi:
class Message {
public:
    Message() : msgID_(0), msgLength_(0), msgBuffer_(nullptr)
    {
    }

    void ProcessOutMsg()
    {
        // …
    }

private:
    unsigned int msgID_;
    unsigned int msgLength_;
    unsigned char* msgBuffer_;
    std::string someIdentifier_; // Varsayılan kurucu fonksiyona sahip, açık başlatmaya gerek yok
};

Öneri 7.1.1 Üye değişkenleri, bildirimde başlatma (C++11) ve kurucu fonksiyon başlatma listesi ile başlatmayı tercih edin

Açıklama: C++11’deki bildirimde başlatma, üye başlangıç değerini açıkça gösterebilir, bu yüzden öncelikli olarak kullanılmalıdır. Eğer üye başlatma değeri kurucu fonksiyonla ilişkiliyse veya C++11 desteklenmiyorsa, kurucu fonksiyon başlatma listesi ile başlatmayı tercih edin. Kurucu fonksiyon gövdesinde üyelere değer atamak yerine başlatma listesi, kodu daha temiz hale getirir, yürütme performansını artırır ve const üyeler ve referans üyeler için başlatma yapabilir.

class Message {
public:
    Message() : msgLength_(0)  // İyi, öncelikle başlatma listesi kullanın
    {
        msgBuffer_ = nullptr;  // Kötü, kurucu fonksiyon içinde değer atamak önerilmez
    }
   
private:
    unsigned int msgID_{0};  // İyi, C++11'de kullanın
    unsigned int msgLength_;
    unsigned char* msgBuffer_;
};

Kural 7.1.2 Örtük dönüşümü önlemek için tek parametreli kurucu fonksiyonları explicit olarak bildirin

Açıklama: Tek parametreli kurucu fonksiyonlar explicit ile bildirilmezse, örtük dönüşüm fonksiyonu olur. Örnek:

class Foo {
public:
    explicit Foo(const string& name): name_(name)
    {
    }
private:
    string name_;
};


void ProcessFoo(const Foo& foo){}

int main(void)
{
    std::string test = "test";
    ProcessFoo(test);  // Derleme başarısız
    return 0;
}

Yukarıdaki kod derleme başarısız olur, çünkü ProcessFoo parametresi Foo tipi gereklidir, string tipi geçirildiğinde eşleşmez.

Foo kurucu fonksiyonunun explicit anahtar kelimesini kaldırırsanız, ProcessFoo çağrılırken string tipi örtük dönüşümü tetikler ve geçici bir Foo nesnesi oluşturur. Bu örtük dönüşüm genellikle kafa karıştırıcıdır ve hataları gizleyebilir, beklenmeyen tip dönüşümüne yol açar. Bu yüzden tek parametreli kurucu fonksiyonlar için explicit bildirimi zorunludur.

Kural 7.1.3 Kopya kurucu fonksiyon, atama operatörü / taşıma kurucu fonksiyon, atama operatörü istemiyorsanız, bunları açıkça yasaklayın

Açıklama: Kullanıcı tanımlı değilse, derleyici varsayılan olarak kopya kurucu fonksiyon ve kopya atama operatörünü, taşıma kurucu fonksiyon ve taşıma atama operatörünü (taşıma semantiği fonksiyonları C++11’den sonra) oluşturur. Eğer kopya kurucu fonksiyon veya atama operatörünü kullanmak istemiyorsanız, bunları açıkça reddedin:

  1. Kopya kurucu fonksiyonu veya atama operatörünü private olarak ayarlayın ve uygulamayın:
class Foo {
private:
    Foo(const Foo&);
    Foo& operator=(const Foo&);
};
  1. C++11’de sağlanan delete kullanın, modern C++ ilgili bölümlerine bakın.
  2. DISALLOW_COPY_AND_MOVE, DISALLOW_COPY, DISALLOW_MOVE gibi makroları kullanmaktan kaçının, NoCopyable, NoMovable’ı miras almanızı öneririz.
class Foo : public NoCopyable, public NoMovable {
};

NoCopyable ve NoMovable’un uygulaması:

class NoCopyable {
public:
    NoCopyable() = default;
    NoCopyable(const NoCopyable&) = delete;
    NoCopyable& operator = (NoCopyable&) = delete;
};

class NoMovable {
public:
    NoMovable() = default;
    NoMovable(NoMovable&&) noexcept = delete;
    NoMovable& operator = (NoMovable&&) noexcept = delete;
};

Kural 7.1.4 Kopya kurucu fonksiyon ve kopya atama operatörü birlikte gelmeli veya yasaklanmalı

Kopya kurucu fonksiyon ve kopya atama operatörü her ikisi de kopyalama anlamına sahiptir, birlikte gelmeli veya yasaklanmalı.

// Birlikte gelir
class Foo {
public:
    ...
    Foo(const Foo&);
    Foo& operator=(const Foo&);
    ...
};

// Birlikte default, C++11 desteklenir
class Foo {
public:
    Foo(const Foo&) = default;
    Foo& operator=(const Foo&) = default;
};

// Birlikte yasakla, C++11 delete kullanılabilir
class Foo {
private:
    Foo(const Foo&);
    Foo& operator=(const Foo&);
};

Kural 7.1.5 Taşıma kurucu fonksiyon ve taşıma atama operatörü birlikte gelmeli veya yasaklanmalı

C++11’de taşıma operasyonu eklendi, eğer bir sınıfın taşıma operasyonunu desteklemesi gerekiyorsa, taşıma kurucu fonksiyon ve taşıma atama operatörünü uygulamanız gerekir.

Taşıma kurucu fonksiyon ve taşıma atama operatörü her ikisi de taşıma anlamına sahiptir, birlikte gelmeli veya yasaklanmalı.

// Birlikte gelir
class Foo {
public:
    ...
    Foo(Foo&&);
    Foo& operator=(Foo&&);
    ...
};

// Birlikte default, C++11 desteklenir
class Foo {
public:
    Foo(Foo&&) = default;
    Foo& operator=(Foo&&) = default;
};

// Birlikte yasakla, C++11 delete kullanılabilir
class Foo {
public:
    Foo(Foo&&) = delete;
    Foo& operator=(Foo&&) = delete;
};

Kural 7.1.6 Kurucu fonksiyon ve yıkıcı fonksiyon içinde sanal fonksiyonları çağırmak yasaktır

Açıklama: Kurucu fonksiyon ve yıkıcı fonksiyon içinde mevcut nesnenin sanal fonksiyonunu çağırmak, çok biçimli davranışa sahip olmayan bir davranışa yol açar. C++‘da, bir taban sınıf bir kez sadece tam bir nesne kurar.

Örnek: Base sınıfı taban sınıfı, Sub sınıfı türetilmiş sınıf

class Base {                      
public:               
    Base();
    virtual void Log() = 0;    // Farklı türetilmiş sınıflar farklı günlük dosyaları çağırır
};

Base::Base()         // Taban sınıf kurucu fonksiyonu
{
    Log();           // Sanal fonksiyon Log'u çağırır
}                                                 

class Sub : public Base {      
public:
    virtual void Log();         
};

Aşağıdaki ifade yürütüldüğünde: Sub sub; önce Sub’un kurucu fonksiyonu yürütülür, ancak önce Base’in kurucu fonksiyonu çağırılır, Base’in kurucu fonksiyonu sanal fonksiyon Log’u çağırır, ancak Log hala taban sınıfın versiyonudur, Base’in kurulması tamamlandıktan sonra türetilmiş sınıfın kurulması tamamlanır, çok biçimli davranışa sahip olmayan bir duruma yol açar. Aynı mantık yıkıcı fonksiyon için de geçerlidir.

Kural 7.1.7 Çok biçimli taban sınıflardaki kopya kurucu fonksiyon, kopya atama operatörü, taşıma kurucu fonksiyon, taşıma atama operatörü public olmayan fonksiyonlar olmalı veya delete fonksiyonlar olmalı

Eğer türetilmiş sınıf nesnesi doğrudan taban sınıf nesnesine atanırsa, dilimleme gerçekleşir, sadece taban sınıf kısmı kopyalanır veya taşınır, çok biçimli davranışa zarar verir. 【Örnek】 Aşağıdaki kodda, taban sınıf kopya kurucu fonksiyon veya kopya atama operatörü tanımlamaz, derleyici bu iki özel üye fonksiyonu otomatik olarak üretir, Eğer türetilmiş sınıf nesnesi taban sınıf nesnesine atanırsa dilimleme gerçekleşir. Kopya kurucu fonksiyon ve kopya atama operatörünü delete olarak bildirebilirsiniz, derleyici bu tür atamaları kontrol eder.

class Base {                      
public:               
    Base() = default;
    virtual ~Base() = default;
    ...
    virtual void Fun() { std::cout << "Base" << std::endl;}
};

class Derived : public Base {
    ...
    void Fun() override { std::cout << "Derived" << std::endl; }
};

void Foo(const Base &base)
{
    Base other = base; // Uygun değil:dilimleme gerçekleşir
    other.Fun(); // Base sınıfının Fun fonksiyonunu çağırır
}
Derived d;
Foo(d); // Türetilmiş sınıf nesnesi geçirilir
  1. Kopya kurucu fonksiyonu veya atama operatörünü private olarak ayarlayın ve uygulamayın:

Miras

Kural 7.2.1 Taban sınıfın yıkıcı fonksiyonu virtual olarak bildirilmeli, miras bırakılmak istenmeyen sınıflar final olarak bildirilmelidir

Açıklama: Yalnızca taban sınıf yıkıcı fonksiyonu virtual olduğunda, çok biçimli çağırma sırasında türetilmiş sınıfın yıkıcı fonksiyonunun çağırıldığı garanti edilir.

Örnek: Taban sınıf yıkıcı fonksiyonu virtual olarak bildirilmediği için bellek sızıntısı oluştu.

class Base {
public:
```cpp
class Sub : public Base {
public:
    Sub() : numbers_(nullptr)
    { 
    }
   
    ~Sub()
    {
        delete[] numbers_;
        std::cout << "~Sub" << std::endl;
    }
   
    int Init()
    {
        const size_t numberCount = 100;
        numbers_ = new (std::nothrow) int[numberCount];
        if (numbers_ == nullptr) {
            return -1;
        }
       
        ...
    }

    std::string getVersion()
    {
        return std::string("hello!");
    }
private:
    int* numbers_;
};
int main(int argc, char* args[])
{
    Base* b = new Sub();

    delete b;
    return 0;
}

Base sınıfının sanal olmayan yıkıcı fonksiyonu olduğu için nesne yok edildiğinde sadece base sınıfın yıkıcı fonksiyonu çağrılır, türetilmiş sınıfın yıkıcı fonksiyonu çağrılmaz, bu da bellek sızmasına neden olur. İstisna: NoCopyable, NoMovable gibi herhangi bir davranışa sahip olmayan ve sadece tanımlayıcı olarak kullanılan sınıflar, sanal yıkıcı veya final tanımlamak zorunda değildir.

Kural7.2.2 Sanal fonksiyonlara varsayılan parametre değerleri vermek yasaktır.

Açıklama: C++‘da sanal fonksiyonlar çalışma zamanında bağlanır, ancak fonksiyonun varsayılan parametreleri derleme zamanında bağlanır. Bu, sonunda yürütülen fonksiyonun türetilmiş sınıfta tanımlanmış, ancak base sınıfın varsayılan parametre değerlerini kullanan bir sanal fonksiyon olduğu anlamına gelir. Sanal fonksiyonların aşırı yüklenmesi sırasında parametre bildirimindeki tutarsızlıktan kaynaklanan kafa karışıklığı ve bunun sonucu ortaya çıkan sorunlardan kaçınmak için, tüm sanal fonksiyonların varsayılan parametre değerleri bildirmemesi gerekir. Örnek: sanal fonksiyon display varsayılan parametre değeri text derleme anında kararlaştırılır, çalışma anında değil. Çok biçimli amaca ulaşmaz:

class Base {
public:
    virtual void Display(const std::string& text = "Base!")
    {
        std::cout << text << std::endl;
    }
   
    virtual ~Base(){}
};

class Sub : public Base {
public:
    virtual void Display(const std::string& text  = "Sub!")
    {
        std::cout << text << std::endl;
    }
   
    virtual ~Sub(){}
};

int main()
{
    Base* base = new Sub();
    Sub* sub = new Sub();
  
    ...
   
    base->Display();  // Program çıktı sonucu: Base! Beklenen çıktı: Sub!
    sub->Display();   // Program çıktı sonucu: Sub!
   
    delete base;
    delete sub;
    return 0;
};

Kural7.2.3 Kalıtımla gelen sanal olmayan fonksiyonların yeniden tanımlanması yasaktır.

Açıklama: Çünkü sanal olmayan fonksiyonlar dinamik bağlama izin vermez, sadece sanal fonksiyonlar dinamik bağlamayı sağlar: base sınıfın pointerına göre işlem yaparak, doğru sonucu elde edebilirsiniz.

Örnek:

class Base {
public:
    void Fun();
};

class Sub : public Base {
public:
    void Fun();
};

Sub* sub = new Sub();                    
Base* base = sub;

sub->Fun();    // Alt sınıfın Fun fonksiyonunu çağırır                 
base->Fun();   // Üst sınıfın Fun fonksiyonunu çağırır
//...

Çoklu kalıtım

Gerçek geliştirme sürecinde çoklu kalıtım kullanımının az olduğu görülür, çünkü çoklu kalıtım kullanım sürecinde aşağıdaki tipik sorunlar vardır:

  1. Elmas kalıtımdan kaynaklanan veri tekrarı ve isim belirsizliği. Bu yüzden C++ virtual kalıtımı sunarak bu tür sorunları çözer;
  2. Elmas kalıtım olmasa bile, birden fazla base sınıf arasındaki isimler çakışabilir ve belirsizlik yaratabilir;
  3. Alt sınıf birden fazla base sınıfın yöntemlerini genişletmesi veya yeniden yazması gerektiğinde, alt sınıfın sorumluluğu belirsiz olur ve anlamsal karmaşa yaratır;
  4. Delegasyona kıyasla, kalıtım bir beyaz kutu yeniden kullanım yöntemidir, yani alt sınıf base sınıfın protected üyelerine erişebilir, bu da daha güçlü bir birleşmeyi tetikler. Çoklu kalıtım ise, birden fazla base sınıfı birleştirdiği için, tek kök kalıtımına kıyasla daha güçlü bir birleşme ilişkisi yaratır.

Çoklu kalıtım aşağıdaki avantajlara sahiptir: Çoklu kalıtım, birden fazla ayrık arayüzün birleştirilmesi ve tekrar kullanılmasını sağlayan daha basit bir kombinasyon sağlar.

Böylece, çoklu kalıtım sadece aşağıdaki birkaç durumda kullanılabilir.

Öneri7.3.1 Arayüz ayrımını ve çoklu rol birleştirmeyi sağlamak için çoklu kalıtım kullanılması

Bir sınıf birden fazla arayüzü uygulaması gerektiğinde, birden fazla ayrı arayüzü birleştirmek için çoklu kalıtım kullanılabilir, Scala dili gibi traits karışımı.

class Role1 {};
class Role2 {};
class Role3 {};

class Object1 : public Role1, public Role2 {
    // ...
};

class Object2 : public Role2, public Role3 {
    // ...
};

C++ standart kütüphanesinde de benzeri bir örnek vardır:

class basic_istream {};
class basic_ostream {};

class basic_iostream : public basic_istream, public basic_ostream {
 
};

Aşırı yükleme

Operatör aşırı yüklemeleri yeterli bir gerekçe olmalıdır ve operatörün orijinal anlamını değiştirmemelidir, örneğin ‘+’ operatörünü eksi işlemi yapmak için kullanmayın. Operatör aşırı yüklemesi, kodu daha sezgisel hale getirir, ancak bazı eksiklikleri de vardır:

  • İçi boş bir algıya sebep olur, bu operatörün dahili tipler gibi yüksek performanslı olduğunu varsayar ve performans düşüşünü göz ardı eder.
  • Sorun giderme sırasında daha sezgisel olmaz, fonksiyon adıyla aramak operatörle aramaktan daha elverişlidir.
  • Operatör aşırı yüklemesi davranış tanımları sezgisel değilse (örneğin ‘+’ operatörünü eksi işlemi yapmak için kullanmak), kodda kafa karışıklığına sebep olur.
  • Atama operatörünün aşırı yüklemesinden kaynaklanan örtük dönüşüm, derin bir gizli hata yaratır. =, == operatörlerini değiştirmek için Equals(), CopyFrom() gibi fonksiyonlar tanımlanabilir.

8 Fonksiyon

Fonksiyon tasarımı

Kural8.1.1 Fonksiyonun çok uzun olmamasına dikkat edin, fonksiyon 50 satırdan (boş olmayan ve yorum olmayan) uzun olmamalıdır.

Fonksiyon tek bir ekranda gösterilebilmeli (50 satır içinde), sadece bir iş yapmalı ve bunu iyi yapmalıdır.

Uzun fonksiyonlar genellikle fonksiyon işlevinin tek başına olmadığı, çok karmaşık olduğu ya da aşırı ayrıntı sunduğu ve daha fazla soyutlamadan yoksun olduğu anlamına gelir.

İstisna: Bazı algoritma uygulayan fonksiyonlar, algoritmanın bütünleşikliği ve işlevin kapsamlılığı nedeniyle 50 satırı aşabilir.

Uzun bir fonksiyonun şu anda çok iyi çalışması dahi olsa, biri tarafından değiştirildiğinde yeni sorunlar ortaya çıkabilir ve hatta keşfedilmesi zor bir hataya sebep olabilir. Bunun yerine, başkalarının kodu okuması ve değiştirmesi için daha kısa ve yönetilebilir fonksiyonlara bölünmesi önerilir.

İç içe fonksiyonlar

Öneri8.2.1 İç içe fonksiyonların uzunluğu 10 satırı (boş olmayan ve yorum olmayan) geçmemelidir.

Açıklama: İç içe fonksiyonlar genel fonksiyonların özelliklerine sahiptir, sadece fonksiyon çağırma işleminde farklılık gösterir. Genel fonksiyon çağırıldığında, program yürütme hakları çağırılan fonksiyona aktarılır ve ardından tekrar çağıran fonksiyona geri döner; iç içe fonksiyon çağırıldığında ise, çağırma ifadesi iç içe fonksiyon gövdesiyle değiştirilir.

İç içe fonksiyonlar sadece 1~10 satır uzunluğundaki küçük fonksiyonlar için uygundur. Çok fazla ifade içeren büyük fonksiyonlarda, fonksiyon çağırma ve dönüş maliyeti göreli olarak önemsizdir ve iç içe fonksiyon kullanmak zorunda değildir, genel derleyici iç içe fonksiyonu görmezden gelir ve fonksiyonu normal bir fonksiyon gibi çağırır.

İç içe fonksiyon karmaşık kontrol yapıları içeriyorsa, döngü, dallanma (switch), try-catch gibi ifadeler, genel derleyici fonksiyonu iç içe fonksiyon yerine normal bir fonksiyon olarak görür. Sanal fonksiyonlar ve özyinelemeli fonksiyonlar iç içe fonksiyon olarak kullanılamaz.

Fonksiyon parametreleri

Öneri8.3.1 Fonksiyon parametrelerinde işaretçi yerine referans kullanılması

Açıklama: Referanslar daha güvenlidir çünkü her zaman boş olmayan ve başka bir hedefe işaret edemeyen bir değerdir; referanslar geçersiz NULL işaretçisinin kontrolünü gerektirmez.

Eğer parametre değiştirilmeyecekse const kullanın, böylece kodu okuyan kişi parametrenin değiştirilmeyeceğini net bir şekilde anlar ve kodun okunabilirliğini büyük ölçüde artırır.

İstisna: Derleme zamanı uzunluğu bilinmeyen bir dizi geçirildiğinde, referans yerine işaretçi kullanılabilir.

Öneri8.3.2 Güçlü tip parametrelerinin kullanılması, void* kullanmaktan kaçınılması

Farklı diller güçlü tip ve zayıf tip arasında kendi görüşlerini verse de, genellikle c/c++ güçlü tip dili olarak kabul edilir, kullandığımız dil güçlü tip olduğuna göre, böyle bir tarzı sürdürmeliyiz. Avantajı, derleyicinin derleme aşamasında tip uyuşmazlığı sorunlarını kontrol etmesini sağlamaktır.

Güçlü tip kullanmak, derleyicinin hataları bulmasına yardımcı olur, aşağıdaki kodda FooListAddNode fonksiyonunun kullanımına dikkat edin:

struct FooNode {
    struct List link;
    int foo;
};

struct BarNode {
    struct List link;
    int bar;
}

void FooListAddNode(void *node) // Bad: burada parametre geçişi için void * tipi kullanıldı
{
    FooNode *foo = (FooNode *)node;
    ListAppend(&g_FooList, &foo->link);
}

void MakeTheList()
{
    FooNode *foo = nullptr;
    BarNode *bar = nullptr;
    ...

    FooListAddNode(bar);        // Wrong: burada foo parametresini geçirmek istendi ama yanlışlıkla bar geçirildi ve hata verilmedi
}
  1. Şablon fonksiyonlar tip değişikliklerini sağlamak için kullanılabilir.
  2. Çok biçimli bir yapı sağlamak için temel sınıf işaretçisi kullanılabilir.

Öneri8.3.3 Fonksiyon parametre sayısı 5’ten fazla olmamalıdır.

Fonksiyonun çok fazla parametresi olması, fonksiyonu dış değişikliklerden etkilenmeye daha açık hale getirir ve bu da bakım çalışmalarını etkiler. Fonksiyonun çok fazla parametresi olması aynı zamanda test çalışmalarını da artırır.

Eğer bu durum aşılırsa aşağıdaki konular düşünülebilir:

  • Fonksiyonu bölmek mümkün mü?
  • İlgili parametreler bir araya getirilip yapı olarak tanımlanabilir mi?

9 C++ diğer özellikleri

Sabitler ve başlatma

Değişmeyen değerler anlamak, izlemek ve analiz etmek daha kolaydır, bu yüzden mümkün olduğunca değişken yerine sabitler kullanılmalıdır. Değer tanımlanırken const’u varsayılan seçenek olarak kullanın.

Kural9.1.1 Sabitleri temsil etmek için makro kullanılması yasaktır

Açıklama: Makrolar basit metin değişiklikleridir, derleme öncesi aşamada yapılır ve çalışma zamanı hataları doğrudan ilgili değeri gösterir; izleme hataları sırasında da değer gösterilir, makro adı değil; makrolar tip kontrolüne sahip değildir, güvenli değildir; makrolar kapsam alanı yoktur.

#define MAX_MSISDN_LEN 20    // kötü örnek

// C++ için const sabitlerin kullanılması gerekir
const int MAX_MSISDN_LEN = 20; // iyi örnek

// C++11 ve sonrası sürümler için constexpr kullanılabilir
constexpr int MAX_MSISDN_LEN = 20;

Öneri9.1.1 Bir grup ilgili tamsayı sabiti enum olarak tanımlanmalıdır.

Açıklama: Enum, #define veya const int daha güvenlidir. Derleyici parametre değerlerinin enum aralığında olup olmadığını kontrol eder ve hataları önlemeye yardımcı olur.

// iyi örnek:
enum Week {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

enum Color {
    RED,
    BLACK,
    BLUE
};

void ColorizeCalendar(Week today, Color color);

ColorizeCalendar(BLUE, SUNDAY); // Derleme hatası, parametre tipi yanlış

// kötü örnek:
const int SUNDAY = 0;
const int MONDAY = 1;

const int BLACK  = 0;
const int BLUE   = 1;

bool ColorizeCalendar(int today, int color);
ColorizeCalendar(BLUE, SUNDAY); // Hata vermez

Enum değerlerin belirli sayısal değerlere karşılık gelmesi gerektiğinde, bildirirken açıkça değer atanmalıdır. Aksi takdirde açıkça değer atanmamalı, değerlerin tekrar atanmasını önlemek ve bakım iş yükünü (eleman ekleme, silme) azaltmak için.

// iyi örnek: S protokolünde cihaz tipini belirtmek için kullanılan cihaz ID değerleri
enum DeviceType {
    DEV_UNKNOWN = -1,
    DEV_DSMP = 0,
    DEV_ISMG = 1,
    DEV_WAPPORTAL = 2
};

Program içi kullanım için, sadece sınıflandırmayı sağlamak amacıyla kullanılan enumlar, açıkça değer atanmamalıdır.

// iyi örnek: oturum durumunu belirtmek için kullanılan enum tanımı
enum SessionState {
    INIT,
    CLOSED,
    WAITING_FOR_RESPONSE
};

Olası durumlarda enum değerlerinin tekrarlanmaması gerekir, eğer tekrarlanması zorunlu ise mevcut enum ile değiştirilmelidir.

enum RTCPType {
    RTCP_SR = 200,
    RTCP_MIN_TYPE = RTCP_SR,       
    RTCP_RR    = 201,
    RTCP_SDES  = 202,
    RTCP_BYE   = 203,
    RTCP_APP   = 204,
    RTCP_RTPFB = 205,
    RTCP_PSFB  = 206,
    RTCP_XR  = 207,
    RTCP_RSI = 208,
    RTCP_PUBPORTS = 209,
    RTCP_MAX_TYPE = RTCP_PUBPORTS 
};

Kural9.1.2 Şeytan sayıların kullanılması yasaktır.

Şeytan sayılar, anlaşılmaz ve zor anlaşılan sayılardır.

Şeytan sayılar siyah beyaz bir kavram değildir, anlaşılmazlığın derecesi de vardır ve bunu kendi başına belirlemek gerekir. Örneğin 12 sayısı, farklı bağlamlarda durum farklıdır: type = 12; anlaşılmazdır, fakat monthsCount = yearsCount * 12; anlaşılır. 0 sayısı bazen de şeytan sayısıdır, örneğin status = 0; hangi durumu temsil ettiğini belirtmez.

Çözüm yolları:

  • Yerel kullanılan sayılara açıklama ekleyin.
  • Birden fazla yerde kullanılan sayılara const sabit tanımlayın ve sembolik adlandırma ile açıklama ekleyin.

Aşağıdaki durumların yapılması yasaktır:

  • Sayıların anlamını açıklamak için sembol kullanmamak, örneğin const int ZERO = 0
  • Sembol adı değerleri kısıtlamak, örneğin const int XX_TIMER_INTERVAL_300MS = 300 doğrudan XX_TIMER_INTERVAL_MS kullanarak zamanlayıcı aralığını belirtmek.

Kural9.1.3 Sabitler tek bir sorumluluğa sahip olmalıdır.

Açıklama: Bir sabit sadece belirli bir işlevi temsil etmeli ve birden fazla işlevi olmamalıdır.

// iyi örnek: Protokol A ve B'de, telefon numarası (MSISDN) uzunluğu 20'dir.
const unsigned int A_MAX_MSISDN_LEN = 20;
const unsigned int B_MAX_MSISDN_LEN = 20;

// veya farklı isim alanları kullanabilirsiniz:
namespace Namespace1 {
    const unsigned int MAX_MSISDN_LEN = 20;
}

namespace Namespace2 {
    const unsigned int MAX_MSISDN_LEN = 20;
}

Kural9.1.4 POD olmayan nesneleri başlatmak için memcpy_s, memset_s kullanılması yasaktır.

Açıklama: POD tam adı Plain Old Data‘dır ve C++ 98 standardı (ISO/IEC 14882, first edition, 1998-09-01) tarafından tanıtılan bir kavramdır, POD tipi çoğunlukla int, char, float, double, enumeration, void, işaretçi gibi orijinal tipleri ve toplu tipleri kapsar, kapsülleme ve nesne yönelimli özelliklerin (örneğin kullanıcı tanımlı kurucu/atama/ yıkıcı fonksiyonlar, taban sınıf, sanal fonksiyonlar vb.) kullanımına izin verilmez.

POD olmayan tipler, örneğin toplu tip olmayan sınıf nesneleri, sanal fonksiyonlara sahip olabilir, bellek düzeni belirsizdir ve derleyiciye bağlıdır, hafıza kopyalama işlemlerinin yaygın kullanımı ciddi sorunlara yol açabilir.

Toplu tip olmayan sınıf için doğrudan hafıza kopyalama ve karşılaştırma işlemleri yapmak, bilgi gizliliği ve veri koruma işlevlerini bozar ve memcpy_s, memset_s işlemlerinin kullanılmasını teşvik etmez.

POD tipi hakkında ayrıntılı açıklama için ek bölümüne bakınız.

Öneri9.1.2 Değişkenler kullanılacakları zaman bildirilmeli ve başlatılmalıdır.

Açıklama: Değişkenlerin ilk değer atamadan kullanılması, yaygın bir düşük seviyeli programlama hatasıdır. Kullanımdan önce değişkenleri bildirmek ve aynı zamanda başlatmak, bu düşük seviyeli hatalardan kaçınmaya yardımcı olur.

Fonksiyonun başında tüm değişkenleri bildirmek, daha sonra değişkenleri kullanmak, kapsamı tüm fonksiyon uygulamasını kapsar ve aşağıdaki sorunlara yol açabilir:

  • Programı anlama ve bakımını zorlaştırır: değişken tanımı ve kullanımı ayrılır.
  • Değişkenlerin uygun bir şekilde başlatılması zordur: fonksiyonun başında, genellikle değişkenleri başlatmak için yeterli bilgi yoktur, çoğu zaman varsayılan boş değerler (örneğin sıfır) ile başlatılır, bu genellikle bir israftır ve değişkenler geçerli bir değere sahip olmadan kullanılırsa hatalara yol açabilir.

Değişken kapsamını en aza indirme ilkesi ve yakın bildirim ilkesi, değişkenin tipini ve ilk değerini anlamayı kolaylaştırır. Özellikle, bildirim ve atamayı birleştirmek için başlatma yöntemini kullanın.

// kötü örnek: bildirim ve başlatma ayrılır
string name;        // bildirimde başlatılmaz: varsayılan kurucu çağrılır
name = "zhangsan";  // tekrar atama operatör fonksiyonu çağrılır; bildirim ve tanım ayrı yerlerde, anlamak zordur

// iyi örnek: bildirim ve başlatma bir arada, anlamak kolaydır
string name("zhangsan");  // kurucu çağrılır

İfadeler

Kural9.2.1 Değişken artırma veya azaltma işlemleri içeren ifadelerde aynı değişkenin tekrar kullanılması yasaktır.

Değişken artırma veya azaltma işlemleri içeren ifadelerde, aynı değişkeni tekrar kullanırsanız, C++ standardı sonucu net bir şekilde tanımlamaz. Farklı derleyiciler veya aynı derleyicinin farklı sürümleri farklı şekilde uygulama yapabilir. Daha iyi taşınabilirlik için, standartta tanımlanmayan işlem sırası hakkında herhangi bir varsayım yapmamalısınız.

Not: parantez kullanmak bu sorunu çözmek için bir çözüm değildir, çünkü bu bir öncelik sorunu değildir.

Örnek:

x = b[i] + i++; // Bad: b[i] işlemi ve i++ arasında öncelik belirsiz.

Doğru yazım şekli, artırma veya azaltma işlemlerini ayrı bir satıra koymaktır:

x = b[i] + i;
i++;            // Good: ayrı bir satır

Fonksiyon parametreleri

Func(i++, i);   // Bad: 2. parametreyi geçirirken, artırma işleminin gerçekleşip gerçekleşmediği belirsiz.

Doğru yazım şekli

i++;            // Good: ayrı bir satır
x = Func(i, i);

Kural9.2.2 switch ifadelerinde default dalları olmalıdır.

Çoğu durumda, switch ifadesinde default dalı olmalıdır, eksik case etiketlerini yakaladığında varsayılan bir işlem sağlar.

İstisna: Switch koşul değişkeni bir enum tipiyse ve case dalları tüm değerleri kapsıyorsa, default dal eklemek gereksiz bir ekleme olabilir. Modern derleyiciler switch ifadesinde eksik enum değerlerinin case dallarını kontrol edebilme yeteneğine sahiptir, buna uygun bir uyarı mesajı verir.

enum Color {
    RED = 0,
    BLUE
};

// Çünkü switch koşul değişkeni bir enum, burada default dalı eklemeye gerek yoktur.
switch (color) {
    case RED:
        DoRedThing();
        break;
    case BLUE:
        DoBlueThing();
        ...
        break;
}

Öneri9.2.1 İfadelerin karşılaştırması, soldaki değişkenin değişken, sağdaki değişkenin sabit olma eğiliminde olması ilkesine uygun olmalıdır.

Değişken sabit değerle karşılaştırıldığında, eğer sabit değer soldaysa, örneğin if (MAX == v) okuma alışkanlıklarına uymaz, if (MAX > v) ise anlamak zordur. İnsanların normal okuma ve ifade alışkanlıklarına göre, sabit değerleri sağa koyun. Şu şekilde yazın:

if (value == MAX) {
 
}

if (value < MAX) {
 
}

Özel durumlar vardır, örneğin: if (MIN < value && value < MAX) aralığı tanımlarken, ilk yarısı sabit değer soldadır.

Sabit değerlerin sol tarafa yerleştirilmesi, ‘==’ operatörünü ‘=’ operatörü ile yanlışlıkla yazmaktan endişe duymak için bir sebep değildir, çünkü if (value = MAX) derleme uyarısı verir, diğer statik kontrol araçları da hata bildirir. Yazım hatalarını önlemek için araçları kullanın, kod okunabilirliği öncelikli olmalıdır.

Öneri9.2.2 Operatör önceliğini belirtmek için parantez kullanılması

Operatör önceliğini belirtmek için parantez kullanın, varsayılan önceliğin tasarım fikriyle uyuşmaması nedeniyle program hatasına yol açmaktan kaçının; aynı zamanda kodu daha okunabilir hale getirin, ancak çok fazla parantez kodun okunabilirliğini azaltır. Parantez kullanımına dair öneriler aşağıdadır.

  • İkili ve daha fazla operatörlerde, eğer birden fazla operatör varsa parantez kullanılmalıdır.
x = a + b + c;         /* Operatör aynı, parantez gerekmez */
x = Foo(a + b, c);     /* İfadelerin iki tarafı virgülle ayrılmış, parantez gerekmez */
x = 1 << (2 + 3);      /* Operatör farklı, parantez gerekir */
x = a + (b / 5);       /* Operatör farklı, parantez gerekir */
x = (a == b) ? a : (a  b);    /* Operatör farklı, parantez gerekir */

Tip dönüşümü

Tip dallarını kullanarak davranışı özelleştirmekten kaçının: Tip dalları kullanarak davranışı özelleştirmek hataya yatkındır ve C kodu yazmaya çalışan bir C++ programcının açık bir işareti olabilir. Bu esnek olmayan bir tekniktir, yeni tipler eklemek istediğinizde, tüm dalları değiştirmeyi unutursanız derleyici size bildiremez. Şablonları ve sanal fonksiyonları kullanın, tiplerin kendilerinin davranışları belirlemesini sağlayın, onları çağıran kodun değil.

Tip dönüşümünü kullanmaktan kaçınmanız önerilir, veri tipleri tasarımında her verinin tipini düşünmelisiniz, tip dönüşümüne çok fazla güvenmek yerine. Temel bir tipi tasarladığınızda, aşağıdaki konuları göz önünde bulundurun:

  • İşaretli mi yoksa işaretsiz mi?
  • float mu yoksa double mu?
  • int8, int16, int32 mi yoksa int64 mu? tam sayı uzunluğunu belirleyin.

Fakat tip dönüşümünü yasaklamak mümkün değildir, çünkü C++ dili makine programlamaya yönelik bir dildir ve işaretçi adresleriyle ilgilenir ve çeşitli üçüncü taraf veya alt seviye API’lerle etkileşime girer, tip tasarımının uygun olmaması durumunda bu uyum sürecinde tip dönüşümüne sıkça rastlanabilir.

İstisna: Bir fonksiyonu çağırdığınızda, fonksiyonun sonucunu işlemek istemediğinizi kesin olarak belirlediyseniz, en iyi seçeneğinizin bu olduğundan emin olun. Eğer fonksiyonun dönüş değerini gerçekten işlemek istemiyorsanız, bunu çözmek için (void) dönüşümünü kullanabilirsiniz.

Kural9.3.1 Eğer tip dönüşümünü kullanmaya karar verdiyseniz, C++ tarafından sağlanan tip dönüşümünü, C tipi dönüşümünü kullanmaktan ziyade tercih edin.

Açıklama:

C++ tarafından sağlanan tip dönüşümü, C tipi dönüşüme göre daha hedefe yönelik, daha okunabilir ve daha güvenlidir. C++ tarafından sağlanan dönüşümler şunları içerir:

  • Tip dönüşümü:
  1. dynamic_cast: çoğunlukla miras sisteminin alt seviye dönüşümü için kullanılır, dynamic_cast tip kontrolüne sahiptir, base sınıf ve türetilmiş sınıf tasarımını iyi yapın, dönüşüm için dynamic_cast kullanımından kaçının.
  2. static_cast: C tipi dönüşüme benzer, değerlerin zorunlu dönüşümü veya yukarı dönüşüm (türetilmiş sınıf işaretçisini veya referansını base sınıf işaretçisine veya referansına dönüştürmek) için kullanılabilir. Bu dönüşüm, çoklu kalıtımın getirdiği tip belirsizliğini gidermek için sıklıkla kullanılır ve göreceli olarak güvenlidir. Eğer sadece aritmetik dönüşümse, o zaman büyük parantezli başlatma yönteminin kullanılmasını öneririz.
  3. reinterpret_cast: ilgisiz tiplerin dönüşümü için kullanılır. reinterpret_cast derleyiciye bir tip nesnenin belleğini başka bir tip olarak yeniden yorumlamasını zorlar, bu güvenli olmayan bir dönüşümdür, reinterpret_cast kullanımını en aza indirmenizi öneririz.
  4. const_cast: nesnenin const özelliğini kaldırmak için kullanılır, nesneyi değiştirilebilir hale getirir, bu veri sabitliğini bozar, kullanımını en aza indirmenizi öneririz.
  • Aritmetik dönüşüm: (C++11’den itibaren desteklenir) Aritmetik dönüşüm ve tip bilgisi kaybolmayan durumlar için, float’tan double’a, int32’ten int64’ya dönüşüm için, büyük parantez başlatma yöntemini kullanmanızı öneririz.
  double d{ someFloat };
  int64_t i{ someInt32 };

Öneri9.3.1 dynamic_cast kullanımından kaçının.

  1. dynamic_cast C++‘ın RTTI’sine dayanır, C++ sınıf nesnelerinin çalışma zamanında tipini tanımlamamızı sağlar.
  2. dynamic_cast‘in ortaya çıkması genellikle base sınıf ve türetilmiş sınıf tasarımımızda bir sorun olduğunu gösterir, türetilmiş sınıf base sınıfın sözleşmesini bozar ve dynamic_cast‘i kullanarak özel işlem yapmaya zorlanırız, bu durumda sınıf tasarımını iyileştirmek yerine dynamic_cast‘i kullanarak sorunu çözmek istemiyoruz.

Öneri9.3.2 reinterpret_cast kullanımından kaçının.

Açıklama: reinterpret_cast ilgisiz tiplerin dönüşümü için kullanılır. reinterpret_cast bir tipi başka bir tip olarak zorlamaya çalışır, bu tip güvenliğini ve güvenilirliğini bozar, güvenli olmayan bir dönüşümdür. Farklı tipler arasında mümkün olduğunca dönüşüm yapmaktan kaçının.

Öneri9.3.3 const_cast kullanımından kaçının.

Açıklama: const_cast nesnenin const ve volatile özelliklerini kaldırmak için kullanılır.

const_cast dönüşümünden sonra elde edilen işaretçi veya referans ile const nesneyi değiştirmek, davranış tanımsızdır.

// kötü örnek
const int i = 1024;
int* p = const_cast<int*>(&i);
*p = 2048;      // tanımsız davranış
// kötü örnek
class Foo {
public:
    Foo() : i(3) {}

    void Fun(int v)
    {
        i = v;
    }

private:
    int i;
};

int main(void)
{
    const Foo f;
    Foo* p = const_cast<Foo*>(&f);
    p->Fun(8);  // tanımsız davranış
}

Kaynak tahsisi ve serbest bırakma

Kural9.4.1 Tek nesne serbest bırakmak için delete kullanın, dizi nesnesi serbest bırakmak için delete [] kullanın.

Açıklama: Tek nesne silme için delete, dizi nesnesi silme için delete [] kullanın, neden:

  • new çağrısının içerdiği işlemler: sistemden bir bellek bloğu talep eder ve bu tipin kurucu fonksiyonunu çağırır.
  • new[n] çağrısının içerdiği işlemler: n tane nesne barındıracak bellek tahsis eder ve her nesne için kurucu fonksiyonu çağırır.
  • delete çağrısının içerdiği işlemler: önce ilgili yıkıcı fonksiyonu çağırır, ardından belleği sisteme iade eder.
  • delete[] çağrısının içerdiği işlemler: her nesne için yıkıcı fonksiyonu çağırır, ardından tüm belleği serbest bırakır.

new ve delete biçimleri eşleşmezse, sonuç bilinmez. Non-class tipi için, new ve delete kurucu ve yıkıcı fonksiyonları çağırmaz.

Yanlış yazım:

const int MAX_ARRAY_SIZE = 100;
int* numberArray = new int[MAX_ARRAY_SIZE];
...
delete numberArray;
numberArray = nullptr;

Doğru yazım:

const int MAX_ARRAY_SIZE = 100;
int* numberArray = new int[MAX_ARRAY_SIZE];
...
delete[] numberArray;
numberArray = nullptr;

Öneri9.4.1 Dinamik tahsisi izlemek için RAII özelliğinin kullanılması

Açıklama: RAII, “kaynak edinimi aynı zamanda başlatmadır” anlamına gelir (Resource Acquisition Is Initialization), nesne yaşam süresini kontrol ederek kaynakları (bellek, dosya tanıtıcıları, ağ bağlantıları, mutex vb.) kontrol etmek için basit bir tekniktir.

RAII’nin genel yaklaşımı şudur: nesne kurulurken kaynak edinilir, ardından nesnenin yaşam süresi boyunca kaynağa erişim kontrol edilir ve kaynağın her zaman geçerli olduğu sağlanır, nihayetinde nesne yıkıcı fonksiyonu çağrıldığında kaynak serbest bırakılır. Bu yaklaşımın iki büyük faydası vardır:

  • Açıkça kaynakları serbest bırakmamıza gerek yoktur.
  • Nesnenin ihtiyaç duyduğu kaynakların nesnenin yaşam süresi boyunca her zaman geçerli olduğu sağlanır. Böylece kaynak geçerliliği sorununu kontrol etme ihtiyacını ortadan kaldırabilir, mantığı basitleştirebilir ve verimliliği artırabilir.

Örnek: RAII kullanarak mutex kaynaklarının açıkça serbest bırakılmasına gerek yoktur.

class LockGuard {
public:
    LockGuard(const LockType& lockType): lock_(lockType)
    {
        lock_.Acquire();
    }
   
    ~LockGuard()
    {
        lock_.Release();
    }
   
private:
    LockType lock_;
};


bool Update()
{
    LockGuard lockGuard(mutex);
    if (...) {
        return false;
    } else {
        // veri üzerinde işlem yap
    }
   
    return true;
}

Standart kütüphane

STL standart şablon kütüphanesinin farklı ürünlerde kullanım oranı farklıdır, burada temel kurallar ve öneriler listelenmiştir, takımların referansı için.

Kural9.5.1 std::string’in c_str() dönüşümünden dönen işaretçiyi saklamayın.

Açıklama: C++ standardında string::c_str() işaretçisinin kalıcı olarak geçerli olduğu belirtilmemiştir, bu nedenle belirli STL uygulamaları string::c_str() çağrıldığında geçici bir depolama alanı döndürebilir ve bunu hızlıca serbest bırakabilir. Bu yüzden taşınabilirliği sağlamak için string::c_str() sonucunu saklamayın, her ihtiyacınız olduğunda doğrudan çağırın.

Örnek:

void Fun1()
{
    std::string name = "demo";
    const char* text = name.c_str();  // İfade bittikten sonra, name'in yaşam süresi hala devam eder, işaretçi geçerlidir

    // Eğer arada string'in const olmayan üye fonksiyonları, örneğin operator[], begin() vb. çağrılırsa
    // string değiştirilmiş olabilir, bu da text'in içeriğinin kullanılamaz hale gelmesine veya artık orijinal dize olmamasına neden olabilir.
    name = "test";
    name[1] = '2';

    // Sonraki text işaretçisinin kullanılması, artık içeriğinin "demo" olmamasına neden olabilir.
}

void Fun2()
{
    std::string name = "demo";
    std::string test = "test";
    const char* text = (name + test).c_str(); // İfade bittikten sonra, + operatörünün ürettiği geçici nesne yok edilir, işaretçi geçersiz olur

    // Sonraki text işaretçisinin kullanılması, artık geçerli bir bellek alanına işaret etmez.
}

İstisna: Performans gereksinimlerinin çok yüksek olduğu bazı kodlarda, zaten tanımlanmış const char* tipine sahip fonksiyonları uyumlandırmak için string::c_str() dönüşümünden dönen işaretçiyi geçici olarak saklamak mümkündür. Ancak string nesnesinin yaşam süresinin, saklanan işaretçinin yaşam süresinden uzun olduğundan ve saklanan işaretçinin yaşam süresi içinde string nesnesinin değiştirilmediğinden emin olunmalıdır.

Öneri9.5.1 char* yerine std::string kullanılması

Açıklama: string’i char* yerine kullanmanın birçok avantajı vardır, örneğin:

  1. Sondaki ‘\0’ ile ilgilenmeye gerek yoktur;
  2. +, =, == gibi operatörleri ve diğer dize işlemleri fonksiyonlarını doğrudan kullanabilirsiniz;
  3. Açık new/delete’e ihtiyaç yoktur, bununla birlikte oluşabilecek hataları önlemek için bellek tahsisasyonu operasyonlarını düşünmenize gerek yoktur;

Dikkat edilmesi gereken şey, bazı STL uygulamalarında string’in yazma zamanı kopyalama stratejisine dayandığıdır, bu iki soruna yol açar, birincisi bazı versiyonların yazma zamanı kopyalama stratejisinin thread-safe olmadığını, çoklu thread ortamında programın çökmesine neden olabileceğini; ikincisi ise thread-safe olmayan bir dinamik kütüphanenin kaldırılmasıyla referans sayımının azaltılamaması ve asılı işaretçiye yol açma ihtimalidir. Bu yüzden, programın stabilitesi için güvenilir bir STL uygulaması seçmek önemlidir.

İstisna: Sistem veya başka üçüncü parti kütüphane API’lerini çağırdığınızda, zaten tanımlanmış arayüzler için sadece char* kullanılabilir. Fakat arayüzü çağırmadan önce string kullanabilirsiniz, arayüzü çağırdığınızda string::c_str() ile karakter işaretçisi elde edebilirsiniz. Yığında karakter dizisi arabellek olarak kullanılacaksa doğrudan karakter dizisi tanımlanabilir, string kullanmaya gerek yoktur ve benzeri bir container olan vector<char> kullanılması da gerekli değildir.

Kural9.5.2 auto_ptr kullanılması yasaktır.

Açıklama: std::auto_ptr’in stdl kütüphanesinde sahip olduğu örtülü sahiplik transfer davranışı vardır, aşağıdaki kod gibi:

auto_ptr<T> p1(new T);
auto_ptr<T> p2 = p1;
  1. satırın yürütülmesinden sonra, p1 artık 1. satırda tahsis edilen nesneye işaret etmez, bunun yerine nullptr olur. Bu yüzden, auto_ptr çeşitli standart containerlarda kullanılamaz. Sahiplik transferi davranışı genellikle beklenen sonuç değildir. Belirli bir sahiplik transferi senaryosunda, örtülü bir transfer yöntemi kullanılmamalıdır. Bu genellikle auto_ptr kullanan kodu incelemek için ekstra dikkat gerektirir, aksi takdirde boş işaretçiye erişim hatası ortaya çıkabilir. auto_ptr’in yaygın kullanımı iki senaryoya ayrılır, birincisi auto_ptr’i fonksiyon dışına üretmek için kullanmak, ikincisi auto_ptr’i RAII yönetim sınıfı olarak kullanmak ve auto_ptr’in yaşam süresi aşıldığında otomatik olarak kaynağı serbest bırakmak. Birinci senaryoda std::shared_ptr kullanılabilir. İkinci senaryoda C++11 standardındaki std::unique_ptr kullanılabilir. std::unique_ptr std::auto_ptr’in yerine geçer ve açık sahiplik transferine izin verir.

İstisna: C++11 standardı yaygın olarak kullanılmadan önce, belirli bir sahiplik transferi gerektiren senaryolarda std::auto_ptr kullanılabilir, ancak auto_ptr’in bir sarmalama sınıfı yapılarak sarmalama sınıfının kopyalama kurucu fonksiyonu ve atama operatörünün kullanımının yasaklanması, bu sarmalama sınıfının standart containerlarda kullanılmamasını sağlamalıdır.

Öneri9.5.2 Yeni standart başlık dosyalarının kullanılması

Açıklama: C++ standart başlık dosyalarını kullandığınızda <cstdlib> gibi başlık dosyaları kullanın, <stdlib.h> gibi başlık dosyaları kullanmayın.

const kullanımı

Değişken veya parametre bildiriminde const anahtar kelimesi ekleme, değişkenin değiştirilemez olduğunu belirtmek için kullanılır (örneğin const int foo). Sınıf fonksiyonlarına const belirteci ekleme, fonksiyonun sınıf veri üyelerinin durumunu değiştirmeyeceğini belirtir (örneğin class Foo { int Bar(char c) const; };). const değişkenler, veri üyeler, fonksiyonlar ve parametreler, derleme zamanı tip kontrolüne ek bir koruma katmanı sağlar, hataları erken tespit etmeyi kolaylaştırır. Bu yüzden, mümkün olduğunca const kullanımını öneririz. Bazen, C++11’in constexpr’ini gerçek sabitleri tanımlamak için kullanmak daha iyi olabilir.

Kural9.6.1 İşaretçi ve referans tipi parametreler için değiştirilmeyecekse const kullanılmalıdır.

Değişmeyen değerler anlamak/izlemek/analiz etmek daha kolaydır, varsayılan seçenek olarak const’u kullanın, derleme zamanı kontrolü ekleyin, böylece kod daha sağlam/daha güvenli olur.

class Foo;

void PrintFoo(const Foo& foo);

Kural9.6.2 Veri üyelerini değiştirmeyen üye fonksiyonlar için const修饰ör kullanılmalıdır.

Mümkün olduğunca üye fonksiyonları const olarak bildirin. Erişim fonksiyonları her zaman const olmalıdır. Veri üyelerini değiştirmeyen üye fonksiyonların hepsi const olarak bildirilmelidir. Sanal fonksiyonlar için, miras zincirindeki tüm sınıfların bu sanal fonksiyonda veri üyelerini değiştirmek ihtiyacı olup olmadığını düşünün, sadece tek bir sınıfın uygulamasına odaklanmayın.

class Foo {
public:

    // ...

    int PrintValue() const // const修饰ör üye fonksiyonu, veri üyelerini değiştirmez
    {
        std::cout << value_ << std::endl;
    }

    int GetValue() const  // const修饰ör üye fonksiyonu, veri üyelerini değiştirmez
    {
        return value_;
    }

private:
    int value_;
};

Öneri9.6.1 Başlatıldıktan sonra değiştirilmeyecek veri üyelerini const olarak tanımlayın.

class Foo {
public:
    Foo(int length) : dataLength_(length) {}
private:
    const int dataLength_; 
};

İstisnalar

Öneri9.7.1 C++11’de, fonksiyon istisna fırlatmayacaksa, noexcept olarak bildirilmelidir.

Gerekçe

  1. Eğer fonksiyon istisna fırlatmayacaksa, noexcept olarak bildirmek derleyicinin fonksiyonu en yüksek seviyede optimize etmesini sağlar, örneğin yürütme yollarını azaltmak, hatalı çıkış verimliliğini artırmak.
  2. vector gibi STL konteynerleri, arayüzün sağlam olması için, elemanların move operatörü noexcept olarak bildirilmemişse, konteyner genişleme eleman taşıma sırasında move mekanizması kullanmak yerine copy mekanizması kullanır, bu da performans kaybı riskine yol açar. Eğer bir fonksiyon istisna fırlatmayacaksa veya bir program belirli bir fonksiyondan fırlatılan istisnayı yakalayıp işlemeyeceksede, bu fonksiyon yeni noexcept anahtar kelimesiyle işaretlenebilir, bu fonksiyonun istisna fırlatmayacağını veya fırlatılan istisnanın yakalanıp işlenmeyeceğini belirtir. Örneğin:
extern "C" double sqrt(double) noexcept;  // asla istisna fırlatmaz

// istisna fırlatabilse de, istisna fırlatılabilir, istisnanın işlenmemesi için fonksiyon noexcept olarak bildirilebilir
// burada bellek tükenme istisnanı işlenmeyecek, basitçe fonksiyon noexcept olarak bildirilebilir
std::vector<int> MyComputation(const std::vector<int>& v) noexcept
{
    std::vector<int> res = v;    // istisna fırlatabilir
    // bir şeyler yap
    return res;
}

Örnek

RetType Function(Type params) noexcept;   // en yüksek optimizasyon
RetType Function(Type params);            // daha az optimizasyon

// std::vector'in move operatörü noexcept bildirilmelidir
class Foo1 {
public:
    Foo1(Foo1&& other);  // noexcept yok
};

std::vector<Foo1> a1;
a1.push_back(Foo1());
a1.push_back(Foo1());  // konteyner genişler, mevcut elemanları taşırken copy constructor çağrılır

class Foo2 {
public:
    Foo2(Foo2&& other) noexcept;
};

std::vector<Foo2> a2;
a2.push_back(Foo2());
a2.push_back(Foo2());  // konteyner genişler, mevcut elemanları taşırken move constructor çağrılır

Not Varsayılan kurucu fonksiyonlar, yıkıcı fonksiyonlar, swap fonksiyonları, move operatörleri istisna fırlatmamalıdır.

Şablon ve genel programlama

Kural9.8.1 OpenHarmony projesinde genel programlama yapılması yasaktır.

Genel programlama ve nesne yönelimli programlamanın düşünce, felsefe ve becerileri tamamen farklıdır, OpenHarmony projesi çoğunlukla nesne yönelimli düşünceyi kullanır.

C++ güçlü genel programlama mekanizmaları sağlar, tip güvenli arayüzlerin çok esnek ve basit bir şekilde uygulanmasını ve farklı tipler ama aynı davranışa sahip kodların tekrar kullanılmasını sağlar.

Fakat C++ genel programlamanın şu dezavantajları vardır:

  1. Genel programlamaya çok aşina olmayanlar, nesne yönelimli mantığı şablonlara, şablonun bağımsız olmayan üyelerini şablonda vb. yazarak mantık karışıklığı, kod şişmesi gibi birçok soruna yol açar.
  2. Şablon programlama, C++‘a çok aşina olmayanlar için oldukça karanlık ve zor anlaşılır. Karmaşık yerlerde şablon kullanılan kodlar daha zor anlaşılır ve debug ve bakım zor olur.
  3. Şablon programlama, kod hatası olduğunda, bu arayüz çok basit olduğunda bile, şablonun karmaşık uygulama ayrıntıları hata mesajında gösterilir. Bu yüzden bu derleme hata mesajı okumak çok zordur.
  4. Şablon yanlış kullanılırsa, çalışma zamanı kod şişmesine yol açabilir.
  5. Şablon kodu değiştirmek ve yeniden düzenlemek zordur. Şablon kodu birçok bağlamda genişletilir, bu yüzden yeniden düzenlemenin bu genişletilmiş kodların hepsine uygun olup olmadığını teyit etmek zordur.

Bu yüzden, OpenHarmony’un çoğu bölümü şablon programlamayı yasaklar, sadece az sayıda bölüm genel programlamayı kullanabilir ve geliştirilen şablonlar ayrıntılı yorumlarla donatılmalıdır. İstisna:

  1. STL adaptör katmanı şablon kullanılabilir

Makrolar

C++ dilinde, karmaşık makroların mümkün olduğunca az kullanılmasını öneririz.

  • Sabit tanımı için, önceki bölümlerde anlatıldığı gibi const veya enum kullanılmalıdır;
  • Makro fonksiyonları için, mümkün olduğunca basit tutun ve aşağıdaki prensiplere uyun, ve tercihen inline fonksiyon, şablon fonksiyon vb. ile değiştirin.
// Makro fonksiyon kullanımını önermiyorum
#define SQUARE(a, b) ((a) * (b))

// Lütfen şablon fonksiyon, inline fonksiyon vb. kullanarak değiştirin.
template<typename T> T Square(T a, T b) { return a * b; }

Makro kullanmanız gerekiyorsa, Lütfen C dili standartları ilgili bölümlerine bakın. İstisna: Bazı yaygın ve olgun uygulamalar, örneğin new, delete’in sarmalanması vb., makro kullanımını koruyabilir.

10 Modern C++ Özellikleri

ISO’nun 2011’de C++11 dil standardını ve 2017 Mart’ta C++17’yi yayınlamasıyla, modern C++ (C++11/14/17 vb.) programlama verimliliğini, kod kalitesini artıran çok sayıda yeni dil özelliği ve standart kütüphane ekledi. Bu bölüm, takımın modern C++‘ı daha etkili kullanmasına, dil tuzaklarından kaçınmasına yardımcı olacak bazı rehberlikleri açıklar.

Kodun Basitliği ve Güvenlik Artışı

Öneri10.1.1 auto‘yu uygun bir şekilde kullanın.

Gerekçe

  • auto, uzun, tekrarlayan tip isimlerini yazmaktan kaçınmaya yardımcı olur, aynı zamanda değişkeni başlatmayı garanti eder.
  • auto tip çıkarma kuralları karmaşıktır, dikkatlice anlaşılması gerekir.
  • Kodu daha temiz hale getirebilecekse, açık tipi kullanmaya devam edin ve sadece yerel değişkenlerde auto kullanın.

Örnek

// Uzun tip isimlerinden kaçının
std::map<string, int>::iterator iter = m.find(val);
auto iter = m.find(val);

// Tekrarlanan tip isimlerinden kaçının
class Foo {...};
Foo* p = new Foo;
auto p = new Foo;

// Başlatmayı garanti edin
int x;    // Derleme başarılı, başlatılmamış
auto x;   // Derleme başarısız, başlatma zorunlu

auto tip çıkarma kafa karıştırıcı olabilir:

auto a = 3;           // int
const auto ca = a;    // const int
const auto& ra = a;   // const int&
auto aa = ca;         // int, const ve reference göz ardı edilir
auto ila1 = { 10 };   // std::initializer_list<int>
auto ila2{ 10 };      // std::initializer_list<int>

auto&& ura1 = x;      // int&
auto&& ura2 = ca;     // const int&
auto&& ura3 = 10;     // int&&

const int b[10];
auto arr1 = b;        // const int*
auto& arr2 = b;       // const int(&)[10]

auto tip çıkarma sırasında referansı göz ardı etmeye dikkat etmezseniz, fark edilmesi zor bir performans sorununa yol açabilir:

std::vector<std::string> v;
auto s1 = v[0];  // auto int olarak çıkarılır, v[0]'ın kopyası alınır

auto ile arayüzü tanımlarsanız, başlık dosyalarındaki sabitler gibi, geliştiriciler değeri değiştirirse tipin değişme olasılığı vardır.

Kural10.1.1 Sanal fonksiyonları yeniden yazarken override veya final anahtar kelimesi kullanılmalıdır.

Gerekçe override ve final anahtar kelimeleri fonksiyonun sanal fonksiyon olduğunu ve temel sınıfın sanal fonksiyonunu yeniden yazdığını garanti eder. Alt sınıf fonksiyonu temel sınıf fonksiyonu prototipiyle uyumsuzsa, derleme uyarısı verir. final aynı zamanda sanal fonksiyonun alt sınıf tarafından yeniden yazılamayacağını garanti eder.

override veya final anahtar kelimeleri kullanılarak temel sınıf sanal fonksiyonu prototipi değiştirilip alt sınıf sanal fonksiyonu yeniden yazmak unutulursa, derleme aşamasında tespit edilebilir. Birden fazla alt sınıf olduğunda, sanal fonksiyonları yeniden yazma değişikliklerinin kaçırılmasını önler.

Örnek

class Base {
public:
    virtual void Foo();
    virtual void Foo(int var);
    void Bar();
};

class Derived : public Base {
public:
    void Foo() const override; // Derleme başarısız: Derived::Foo ve Base::Foo prototipi uyumsuz, yeniden yazma değil
    void Foo() override;       // Doğru: Derived::Foo Base::Foo'yu yeniden yazar
    void Foo(int var) final;   // Doğru: Derived::Foo(int) Base::Foo(int)'i yeniden yazar ve Derived'in alt sınıfları bu fonksiyonu yeniden yazamaz
    void Bar() override;       // Derleme başarısız: Base::Bar sanal fonksiyon değil
};

Özet

  1. Temel sınıfın ilk sanal fonksiyon tanımı, virtual anahtar kelimesi kullanır
  2. Alt sınıf temel sınıf sanal fonksiyonunu (yıkıcı fonksiyon dahil) yeniden yazdığında, override veya final anahtar kelimesi kullanır (fakat ikisi birden kullanılmaz), ve virtual anahtar kelimesi kullanılmaz
  3. Sanal olmayan fonksiyonlar, virtual, override ve final anahtar kelimeleri kullanılmaz

Kural10.1.2 delete anahtar kelimesi fonksiyonları silmek için kullanılmalıdır.

Gerekçe Sınıf üye fonksiyonlarını private olarak bildirmek ve uygulamamak yerine delete anahtar kelimesi daha net ve daha geniş kapsamlı bir etkiye sahiptir.

Örnek

class Foo {
private:
    // Başlık dosyasına bakarak kopyalama kurucu fonksiyonun silinip silinmediği bilinemez
    Foo(const Foo&);
};

class Foo {
public:
    // Açıkça kopyalama atama fonksiyonunu sil
    Foo& operator=(const Foo&) = delete;
};

delete anahtar kelimesi aynı zamanda non-üye fonksiyonları silmek için kullanılabilir

template<typename T>
void Process(T value);

template<>
void Process<void>(void) = delete;

Kural10.1.3 NULL veya 0 yerine nullptr kullanılmalıdır.

Gerekçe Uzun zamandır, C++ boş işaretçi için bir anahtar kelimeye sahip değildir, bu çok utandırıcı bir durumdur:

#define NULL ((void *)0)

char* str = NULL;   // Hata: void* otomatik olarak char* olarak dönüştürülemez

void(C::*pmf)() = &C::Func;
if (pmf == NULL) {} // Hata: void* otomatik olarak üye fonksiyon işaretçisine dönüştürülemez

Eğer NULL 0 veya 0L olarak tanımlanırsa, yukarıdaki sorunları çözebilir.

Ya da boş işaretçiye ihtiyacınız olduğunda doğrudan 0 kullanabilirsiniz. Fakat bu başka bir soruna yol açar, kod net değil, özellikle auto otomatik çıkarma kullandığınızda:

auto result = Find(id);
if (result == 0) {  // Find() int mi yoksa işaretçi mi döndürüyor?
    // bir şeyler yap
}

0 sözlü olarak int tipidir (0L ise long), bu yüzden NULL ve 0 işaretçi tipi değildir. İşaretçi ve tam sayı tiplerinin fonksiyonları aşırı yüklediğinizde, NULL veya 0 ile fonksiyonları çağırırken işaretçi tipi aşırı yüklenmiş fonksiyon yerine tam sayı tipi aşırı yüklenmiş fonksiyonu çağırırsınız:

void F(int);
void F(int*);

F(0);      // F(int) fonksiyonunu çağırır, F(int*) fonksiyonunu çağırmaz
F(NULL);   // F(int) fonksiyonunu çağırır, F(int*) fonksiyonunu çağırmaz

Ayrıca, sizeof(NULL) == sizeof(void*) her zaman doğru olmayabilir, bu da potansiyel bir risktir.

Özet: 0 veya 0L doğrudan kullanmak, kodu netleştirmez ve tip güvenliğini sağlamaz; NULL tip güvenliğini sağlamaz. Bunların hepsi potansiyel risklerdir.

nullptr‘in avantajı sadece sözlü olarak boş işaretçi temsil etmekle kalmaz, aynı zamanda kodu netleştirir ve tip güvenliğini sağlar.

nullptr, std::nullptr_t tipidir ve std::nullptr_t tüm temel işaretçi tiplerine örtülü olarak dönüştürülebilir, bu nullptr‘in herhangi tip işaretçiye boş işaretçi olarak davranmasını sağlar.

void F(int);
void F(int*);
F(nullptr);   // F(int*) fonksiyonunu çağırır

auto result = Find(id);
if (result == nullptr) {  // Find() işaretçi döndürüyor
    // bir şeyler yap
}

Kural10.1.4 typedef yerine using kullanılmalıdır.

C++11‘den önce, typedef ile tip takma adları tanımlanabilir. Kimse std::map<uint32_t, std::vector<int>> gibi kodları tekrar tekrar yazmak istemez.

typedef std::map<uint32_t, std::vector<int>> SomeType;

Tip takma adları aslında tiplerin kapsüllemesidir. Kapsülleme aracılığıyla, kod daha temiz hale getirilebilir ve büyük ölçüde tip değişikliklerinden kaynaklanan dağılımlı değişikliklerden kaçınılabilir. C++11‘den sonra, using ile alias declarations tanımlanabilir:

using SomeType = std::map<uint32_t, std::vector<int>>;

İki formatı karşılaştırın:

typedef Type Alias;   // Tip önce mi, yoksa Alias önce mi
using Alias = Type;   // 'atama' kullanımına uygun, anlaşılmaz ve hatalı olmaz

Eğer bu kadarı using’e geçmek için yeterli değilse, alias template‘e bakalım:

// Bir şablonun takma adı tanımlamak, tek satır kod
template<class T>
using MyAllocatorVector = std::vector<T, MyAllocator<T>>;

MyAllocatorVector<int> data;       // using ile tanımlanan takma adı kullanmak

template<class T>
class MyClass {
private:
    MyAllocatorVector<int> data_;   // Şablon sınıfı içinde using ile tanımlanan takma adı kullanmak
};

typedef şablon parametresi olan takma adları desteklemez, sadece “dolaylı yol” izlenebilir:

// typedef ile şablonu sarmalamak, bir şablon sınıfı uygulamak gerekir
template<class T>
struct MyAllocatorVector {
    typedef std::vector<T, MyAllocator<T>> type;
};

MyAllocatorVector<int>::type data;  // typedef ile tanımlanan takma adı kullanmak, ::type eklemek gerekir

template<class T>
class MyClass {
private:
    typename MyAllocatorVector<int>::type data_;  // Şablon sınıfı içinde kullanmak, ::type eklemek gerekir ve typename eklemek gerekir
};

Kural10.1.5 const nesneler üzerinde std::move operasyonu yapmak yasaktır.

Yüzeyde, std::move bir nesneyi hareket ettirmek anlamına geliyor. const nesneler değiştirilemez, bu yüzden doğal olarak hareket ettirilemez. Bu yüzden const nesneler üzerinde std::move kullanmak kod okuyucular için kafa karıştırıcı olur. Gerçek fonksiyonel olarak, std::move nesneyi sağ değer referansına dönüştürür; const nesneler için const sağ değer referansına dönüştürülür. Çoğu zaman const sağ değer referansına sahip hareket kurucu fonksiyon ve hareket atama operatörü tanımlanmaz, bu yüzden kod fonksiyonel olarak nesneyi kopyalamaya düşer, bu da performans kaybına yol açar.

Yanlış örnek:

std::string g_string;
std::vector<std::string> g_stringList;

void func()
{
    const std::string myString = "String content";
    g_string = std::move(myString); // kötü: myString hareket ettirilmez, kopyalanır
    const std::string anotherString = "Another string content";
    g_stringList.push_back(std::move(anotherString));    // kötü: anotherString hareket ettirilmez, kopyalanır
}

Akıllı işaretçiler

Kural10.2.1 Sahipliği birden fazla taraf tarafından tutulmayan singleton, sınıf üyeleri vb. için akıllı işaretçi yerine raw işaretçi kullanılmalıdır.

Gerekçe Akıllı işaretçiler otomatik olarak nesne kaynaklarını serbest bırakır ve kaynak sızıntısını önler, ancak ek kaynak maliyetlerine sahiptir. Örneğin: akıllı işaretçi tarafından otomatik olarak oluşturulan sınıflar, kurma ve yıkma maliyetleri, daha fazla bellek kullanımı vb. Singleton, sınıf üyeleri vb. nesnelerin sahipliği birden fazla taraf tarafından tutulmayan durumlarda, sınıf yıkıcıda kaynağın serbest bırakılması yeterlidir. Ek maliyetleri önlemek için akıllı işaretçi kullanılmamalıdır.

Örnek

class Foo;
class Base {
public:
    Base() {}
    virtual ~Base()
    {
        delete foo_;
    }
private:
    Foo* foo_ = nullptr;
};

İstisna

  1. Oluşturulan nesneyi döndürürken ve nesne için işaretçi yıkıcı fonksiyonu gerekiyorsa akıllı işaretçi kullanılabilir.
class User;
class Foo {
public:
    std::unique_ptr<User, void(User *)> CreateUniqueUser() // Nesnenin oluşturulması ve serbest bırakılmasının aynı runtime'da olduğundan emin olmak için unique_ptr kullanılabilir
    {
        sptr<User> ipcUser = iface_cast<User>(remoter);
        return std::unique_ptr<User, void(User *)>(::new User(ipcUser), [](User *user) {
            user->Close();
            ::delete user;
        });
    }

    std::shared_ptr<User> CreateSharedUser() // Nesnenin oluşturulması ve serbest bırakılmasının aynı runtime'da olduğundan emin olmak için shared_ptr kullanılabilir
    {
        sptr<User> ipcUser = iface_cast<User>(remoter);
        return std::shared_ptr<User>(ipcUser.GetRefPtr(), [ipcUser](User *user) mutable {
            ipcUser = nullptr;
        });
    }
};
  1. Oluşturulan nesneyi döndürürken ve nesnenin birden fazla taraf tarafından referanslanması gerektiğinde shared_ptr kullanılabilir.

Kural10.2.2 std::make_unique kullanarak unique_ptr oluşturun, new yerine.

Gerekçe

  1. make_unique daha temiz bir oluşturma yöntemidir.
  2. Karmaşık ifadelerde istisna güvenliği sağlar.

Örnek

// Kötü: MyClass iki kez görülür, tutarsızlık riskine yol açar.
std::unique_ptr<MyClass> ptr(new MyClass(0, 1));
// İyi: MyClass sadece bir kez görülür, tutarsızlık olasılığı yoktur.
auto ptr = std::make_unique<MyClass>(0, 1);

Tipin tekrarlanması çok ciddi bir soruna yol açabilir ve zor bulunabilir:

// Derleme başarılı, ama new ve delete uyumsuz
std::unique_ptr<uint8_t> ptr(new uint8_t[10]);
std::unique_ptr<uint8_t[]> ptr(new uint8_t);
// İstisna güvenli değil: Derleyici fonksiyon parametrelerini şu sırayla hesaplayabilir:
// 1. Foo için bellek ayırır,
// 2. Foo kurucu fonksiyonunu çağırır,
// 3. Bar fonksiyonunu çağırır,
// 4. unique_ptr<Foo> kurucu fonksiyonunu çağırır.
// Eğer Bar istisna fırlatırsa, Foo yok edilmez ve bellek sızıntısı olur.
F(unique_ptr<Foo>(new Foo()), Bar());

// İstisna güvenli: Fonksiyon çağrısı kesintiye uğramaz.
F(make_unique<Foo>(), Bar());

İstisna std::make_unique özel deleter desteklemez. Özel deleter gerektiren senaryolarda, kendi isim alanınızda özelleştirilmiş make_unique sürümünü uygulamanız önerilir. Özel deleter ile unique_ptr oluşturmak için new kullanmak son çaredir.

Kural10.2.4 std::make_shared kullanarak shared_ptr oluşturun, new yerine.

Gerekçe std::make_shared kullanarak std::shared_ptr oluşturmak, std::make_unique gibi tutarlılık vb. nedenlerin yanı sıra performans nedenleri de vardır. std::shared_ptr iki varlığı yönetir:

  • Kontrol bloğu (referans sayımı, deleter vb. saklar)
  • Yönetilen nesne

std::make_shared kullanarak std::shared_ptr oluşturmak, kontrol bloğu ve yönetilen nesneyi barındıracak yeterli belleği bir seferde heap’tan ayırır. std::shared_ptr<MyClass>(new MyClass) kullanarak std::shared_ptr oluşturmak, new MyClass heap ayırma tetiklerinin yanı sıra, std::shared_ptr kurucu fonksiyonu da ikinci heap ayırma tetikler, ek maliyet yaratır.

İstisna std::make_shared gibi özel deleter desteklemez.

Lambda

Öneri10.3.1 Fonksiyonlar çalışmadığında lambda (yerel değişkenleri yakalama veya yerel fonksiyon yazma) kullanmayı tercih edin.

Gerekçe Fonksiyonlar yerel değişkenleri yakalayamaz veya yerel kapsamda bildirilemez; eğer bunlara ihtiyaç duyarsanız, functor yerine mümkün olduğunca lambda kullanın. Öte yandan, lambda ve functor aşırı yüklenemez; eğer aşırı yüklemeye ihtiyaç duyarsanız, fonksiyonları kullanın. lambda ve fonksiyonlar aynı anda kullanılabilirse, fonksiyonları tercih edin; mümkün olduğunca en basit aracı kullanın.

Örnek

// Sadece int veya string alan bir fonksiyon yazın
// -- aşırı yükleme doğal bir seçimdir
void F(int);
void F(const string&);

// Yerel durumu yakalamak veya ifade veya ifade kapsamında görünmek için
// -- lambda doğal bir seçimdir
vector<Work> v = LotsOfWork();
for (int taskNum = 0; taskNum < max; ++taskNum) {
    pool.Run([=, &v] {...});
}
pool.Join();

Kural10.3.1 Yerel kapsam dışı lambdas kullanın, referans ile yakalamaktan kaçının.

Gerekçe Yerel kapsam dışı lambdas arasında değerler, heap’ta saklananlar veya başka threadlere geçirilenler bulunur. Yerel işaretçiler ve referanslar yerel değişkenlerin ötesinde var olmamalıdır. lambdas referans ile yakalama, yerel nesnelerin referansını saklamak anlamına gelir. Eğer bu, yerel değişkenlerin yaşam süresinden daha uzun referansların var olmasına yol açarsa, referans ile yakalamaktan kaçınılmalıdır.

Örnek

// Kötü
void Foo()
{
    int local = 42;
    // Referans ile local'i yakalar.
    // Fonksiyon döndükten sonra, local artık mevcut değildir,
    // bu yüzden Process() çağrısının davranışı tanımsızdır!
    threadPool.QueueWork([&]{ Process(local); });
}

// İyi
void Foo()
{
    int local = 42;
    // Değer ile local'i yakalar.
    // local'in kopyalandığından emin olun, Process() çağrısı sırasında local her zaman geçerlidir.
    threadPool.QueueWork([=]{ Process(local); });
}

Öneri10.3.2 this‘i yakalarsanız, tüm değişkenleri açıkça yakalayın.

Gerekçe Üye fonksiyonundaki [=] değer ile yakalama gibi görünür. Fakat örtülü olarak this işaretçisini değer ile aldığını ve tüm üye değişkenleri işleyebildiğini unutmayın, bu genellikle kaçınılmalıdır. Eğer bunu yapmaya gerçekten ihtiyacınız varsa, this‘i açıkça belirtin.

Örnek

class MyClass {
public:
    void Foo()
    {
        int i = 0;

        auto Lambda = [=]() { Use(i, data_); };   // Kötü: kopyalama/ değer yakalama gibi görünür, üye değişkenler aslında referans ile yakalanır.

        data_ = 42;
        Lambda(); // use(42) çağrısı yapar;
        data_ = 43;
        Lambda(); // use(43) çağrısı yapar;

        auto Lambda2 = [i, this]() { Use(i, data_); }; // İyi, açıkça değer ile yakalama belirtilmiştir, en açık, en az karışıklık.
    }

private:
    int data_ = 0;
};

Öneri10.3.3 Varsayılan yakalama modlarını kullanmaktan kaçının.

Gerekçe Lambda ifadeleri iki varsayılan yakalama modu sağlar: referansla (&) ve değerle (=). Varsayılan referansla yakalama, örtülü olarak tüm yerel değişkenlerin referansını yakalar ve asılı referansların erişimine yol açabilir. Buna karşı, belirli bir değişkeni yakalamak için açıkça yazmak, nesne yaşam süresini kontrol etmeyi kolaylaştırır ve hataları azaltır. Varsayılan değerle yakalama örtülü olarak this işaretçisini yakalar ve lambda fonksiyonunun bağımlı olduğu değişkenlerin hangileri olduğu hakkında belirsizlik yaratabilir. Eğer statik bir değişken varsa, lambda’nın statik değişkenin bir kopyasını aldığını yanlış anlayabilir. Bu yüzden, genellikle lambda’nın yakalaması gereken değişkenleri açıkça yazmalı ve varsayılan yakalama modlarını kullanmamalısınız.

Yanlış örnek

auto func()
{
    int addend = 5;
    static int baseValue = 3;

    return [=]() {  // Aslında sadece addend'i kopyalar
        ++baseValue;    // Statik değişkenin değerini etkiler
        return baseValue + addend;
    };
}

Doğru örnek

auto func()
{
    int addend = 5;
    static int baseValue = 3;

    return [addend, baseValue = baseValue]() mutable {  // C++14'teki capture initialization ile bir kopya değişkeni kullanın
        ++baseValue;    // Statik değişkenin değerini etkilemez, kendi kopyasını değiştirir
        return baseValue + addend;
    };
}

Referans: “Effective Modern C++”: Item 31: Avoid default capture modes.

Arayüz

Öneri10.4.1 Sahiplikla ilgili olmayan senaryolarda, akıllı işaretçi yerine T* veya T& parametre olarak kullanılmalıdır.

Gerekçe

  1. Sadece açık bir sahiplik mekanizması gerektiğinde, akıllı işaretçi ile sahipliği transfer veya paylaşın.
  2. Akıllı işaretçi ile parametreyi geçirmek, fonksiyon çağırıcıyı akıllı işaretçi kullanmaya zorlar (örneğin, çağırıcı this geçirmek istiyor).
  3. Paylaşılan sahiplik akıllı işaretçisinin çalışma zamanı maliyeti vardır.

Örnek

// Herhangi bir int* kabul eder
void F(int*);

// Sadece sahipliği transfer etmek isteyen int kabul eder
void G(unique_ptr<int>);

// Sadece sahipliği paylaşmak isteyen int kabul eder
void G(shared_ptr<int>);

// Sahiplik değiştirmeyen, ancak belirli bir sahiplikteki çağırıcıyı gerektiren
void H(const unique_ptr<int>&);

// Herhangi bir int kabul eder
void H(int&);

// Kötü
void F(shared_ptr<Widget>& w)
{
    // ...
    Use(*w); // Sadece w'yu kullanır -- tamamen yaşam süresi yönetimiyle ilgili değildir
    // ...
};