← Kursa Dön
📄 Text · 15 min

Cross-Platform C++ Geliştirme

Bir restoran zinciri düşün. İstanbul'daki şubede döner, Tokyo'dakinde sushi, New York'takinde burger satılıyor — ama hepsinin aynı POS sistemi, aynı muhasebe yazılımı, aynı sipariş akışı var. Menü değişiyor, ama altyapı aynı. Cross-platform yazılım geliştirmek de tam olarak bu: farklı işletim sistemlerinde çalışan, ama tek bir kod tabanından yönetilen yazılım üretmek. Windows, Linux ve macOS'un farklı "menüleri" var — farklı sistem çağrıları, farklı dosya yolları, farklı derleyici davranışları. Ama iyi tasarlanmış bir C++ projesi, bu farklılıkları soyutlayarak tek bir kod tabanıyla hepsinde çalışabilir.

C++ tarihsel olarak "platformun metal'ine yakın" bir dil. Bu güç aynı zamanda bir sorumluluk: platform farklılıklarını kendin yönetmek zorundasın. Ama modern C++ (C++17 ve sonrası) ve CMake gibi araçlarla bu iş artık eskisi kadar acı verici değil.


Neden Platform Bağımsız Kod Önemli?

İş Dünyası Perspektifi

Yazılımın sadece Windows'ta çalışması, potansiyel kullanıcı kitlenin büyük bir kısmını dışarıda bırakmak demek. Sunucu tarafında Linux baskın, masaüstünde macOS ciddi bir paya sahip, gömülü sistemlerde ise her şey farklı. Tek platform hedeflemek, kendini kısıtlamaktır.

Teknik Perspektif

Platform bağımsız yazılan kod doğal olarak daha temiz olur. Neden? Çünkü platform bağımlılıklarını soyutlamak, kodu modülerleştirmeye zorlar. Bir Windows API çağrısını doğrudan iş mantığının içine gömmek yerine, bir wrapper arkasına saklamak gerekir — bu da daha test edilebilir, daha bakımı kolay kod demektir.

Gerçek Dünya Örnekleri

  • LLVM/Clang: Windows, Linux, macOS, hatta FreeBSD'de çalışır

  • Qt: Tek kod tabanıyla masaüstü, mobil ve gömülü platformlarda GUI

  • CMake: Kendisi de cross-platform olan bir build sistemi

  • Game engine'ler: Unreal, Godot — PC, konsol, mobil


Preprocessor Macro'lar: İlk Savunma Hattı

Her işletim sistemi, derleyici tarafından otomatik tanımlanan önceden belirlenmiş makrolar (predefined macros) sağlar. Bu makrolar, kodun hangi platformda derlendiğini derleme zamanında bilmeni sağlar.

Platform Algılama Makroları

#include <iostream>
#include <string>

std::string get_platform_name() {
    #if defined(_WIN32)
        return "Windows";
    #elif defined(__APPLE__) && defined(__TARGET_OS_MAC)
        return "macOS";
    #elif defined(__linux__)
        return "Linux";
    #elif defined(__FreeBSD__)
        return "FreeBSD";
    #else
        return "Unknown";
    #endif
}

std::string get_compiler_name() {
    #if defined(_MSC_VER)
        return "MSVC " + std::to_string(_MSC_VER);
    #elif defined(__clang__)
        return "Clang " + std::to_string(__clang_major__) + "." 
               + std::to_string(__clang_minor__);
    #elif defined(__GNUC__)
        return "GCC " + std::to_string(__GNUC__) + "." 
               + std::to_string(__GNUC_MINOR__);
    #else
        return "Unknown Compiler";
    #endif
}

int main() {
    std::cout << "Platform: " << get_platform_name() << "\n";
    std::cout << "Compiler: " << get_compiler_name() << "\n";
    return 0;
}

Burada kritik bir detay var: __APPLE__ makrosu hem macOS hem iOS için tanımlıdır. macOS'u spesifik olarak hedeflemek istiyorsan __TARGET_OS_MAC'i de kontrol etmen gerekir. Benzer şekilde _WIN32, hem 32-bit hem 64-bit Windows'ta tanımlıdır — 64-bit spesifik kontrol için _WIN64 kullanılır.

Yaygın Platform Makroları Tablosu

MakroPlatform
_WIN32Windows (32 ve 64-bit)
_WIN64Windows 64-bit
__linux__Linux
__APPLE__macOS / iOS
__ANDROID__Android (NDK)
__FreeBSD__FreeBSD
_MSC_VERMSVC derleyici
__GNUC__GCC derleyici
__clang__Clang derleyici

Dikkatli Kullan

Preprocessor makrolar güçlüdür ama kontrolsüz kullanım kodu okunmaz hale getirir. İç içe #ifdef'ler, spagetti kodun makro versiyonudur. Hedef: makro kullanımını mümkün olduğunca az dosyada, mümkün olduğunca soyut tutmak.


Platform-Specific API Wrapper Yazma

Platform farklılıklarını yönetmenin en temiz yolu, ortak bir arayüz tanımlayıp her platform için ayrı implementasyon sağlamaktır. Bu yaklaşım makro kirliliğini birkaç dosyayla sınırlar.

Header-Based Yaklaşım

// platform/sleep.h — Ortak arayüz
#pragma once
#include <cstdint>

namespace platform {
    // Belirtilen milisaniye kadar bekle
    void sleep_ms(uint32_t milliseconds);
    
    // Yüksek çözünürlüklü zaman damgası (nanosaniye)
    uint64_t high_res_timestamp();
}
// platform/sleep_win32.cpp — Windows implementasyonu
#ifdef _WIN32
#include "sleep.h"
#include <windows.h>

namespace platform {

void sleep_ms(uint32_t milliseconds) {
    Sleep(milliseconds);  // Windows API
}

uint64_t high_res_timestamp() {
    LARGE_INTEGER freq, counter;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&counter);
    return static_cast<uint64_t>(counter.QuadPart * 1000000000ULL / freq.QuadPart);
}

} // namespace platform
#endif
// platform/sleep_posix.cpp — Linux/macOS implementasyonu
#if defined(__linux__) || defined(__APPLE__)
#include "sleep.h"
#include <unistd.h>
#include <time.h>

namespace platform {

void sleep_ms(uint32_t milliseconds) {
    usleep(milliseconds * 1000);  // POSIX API (mikrosaniye alır)
}

uint64_t high_res_timestamp() {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return static_cast<uint64_t>(ts.tv_sec) * 1000000000ULL + ts.tv_nsec;
}

} // namespace platform
#endif

Bu yapıda iş mantığı kodun sadece platform::sleep_ms(100) çağırır. Hangi platformda olduğunu bilmesine gerek yok. CMake tarafında ise doğru .cpp dosyası derlemeye dahil edilir.

CMake ile Dosya Seçimi

# CMakeLists.txt
if(WIN32)
    set(PLATFORM_SOURCES platform/sleep_win32.cpp)
elseif(APPLE)
    set(PLATFORM_SOURCES platform/sleep_posix.cpp)
elseif(UNIX)
    set(PLATFORM_SOURCES platform/sleep_posix.cpp)
endif()

add_library(platform_lib ${PLATFORM_SOURCES})
target_include_directories(platform_lib PUBLIC platform/)

Bu yaklaşım, makroları iş mantığından tamamen ayırır. Sadece platform katmanı makrolarla uğraşır, geri kalan kod tamamen temiz kalır.

Not: usleep() POSIX.1-2001'de tanımlıdır ama POSIX.1-2008'de deprecated olmuştur. Modern POSIX kodda nanosleep() tercih edilmelidir.


std::filesystem — Platform Bağımsız Dosya İşlemleri

C++17 ile gelen std::filesystem kütüphanesi, dosya sistemi işlemlerini platform bağımsız hale getiren en önemli modern C++ özelliklerinden biridir. Öncesinde, dosya varlığını kontrol etmek için bile platform-spesifik API kullanmak gerekiyordu: Windows'ta GetFileAttributes(), POSIX'te stat().

Temel İşlemler

#include <iostream>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

void explore_filesystem() {
    // Yol oluşturma — otomatik separator kullanır
    // Windows'ta '\', Linux/macOS'ta '/'
    fs::path project_dir = fs::current_path() / "my_project";
    fs::path config_file = project_dir / "config" / "settings.json";
    
    std::cout << "Proje dizini: " << project_dir << "\n";
    std::cout << "Config dosyası: " << config_file << "\n";
    std::cout << "Dosya adı: " << config_file.filename() << "\n";
    std::cout << "Uzantı: " << config_file.extension() << "\n";
    std::cout << "Parent: " << config_file.parent_path() << "\n";
    
    // Dizin oluşturma (iç içe dizinleri de oluşturur)
    fs::create_directories(project_dir / "config");
    fs::create_directories(project_dir / "src");
    fs::create_directories(project_dir / "build");
    
    // Dosya oluştur
    std::ofstream out(config_file);
    out << R"({"version": "1.0", "debug": true})";
    out.close();
    
    // Dosya bilgilerini sorgula
    if (fs::exists(config_file)) {
        auto size = fs::file_size(config_file);
        auto time = fs::last_write_time(config_file);
        std::cout << "Dosya boyutu: " << size << " byte\n";
        
        auto is_regular = fs::is_regular_file(config_file);
        std::cout << "Regular file: " << std::boolalpha << is_regular << "\n";
    }
    
    // Dizin içeriğini listele
    std::cout << "\nProje yapısı:\n";
    for (const auto& entry : fs::recursive_directory_iterator(project_dir)) {
        auto relative = fs::relative(entry.path(), project_dir);
        auto depth = std::distance(relative.begin(), relative.end()) - 1;
        std::string indent(depth * 2, ' ');
        
        if (entry.is_directory()) {
            std::cout << indent << "[" << entry.path().filename().string() << "]/\n";
        } else {
            std::cout << indent << entry.path().filename().string() 
                      << " (" << entry.file_size() << " bytes)\n";
        }
    }
    
    // Temizlik
    fs::remove_all(project_dir);
}

int main() {
    explore_filesystem();
    return 0;
}

Path Sınıfının Gücü

fs::path'in en güzel yanı / operatörünün yol birleştirme için overload edilmiş olması. project_dir / "config" / "settings.json" yazımı, platform farkını tamamen soyutlar. Windows'ta \, diğer platformlarda / kullanır.

#include <iostream>
#include <filesystem>
#include <vector>
#include <algorithm>

namespace fs = std::filesystem;

// Belirli uzantıdaki dosyaları recursive bul
std::vector<fs::path> find_files(const fs::path& root, const std::string& extension) {
    std::vector<fs::path> results;
    
    if (!fs::exists(root) || !fs::is_directory(root)) {
        return results;
    }
    
    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        if (entry.is_regular_file() && entry.path().extension() == extension) {
            results.push_back(entry.path());
        }
    }
    
    // Sıralı döndür (tekrarlanabilirlik için)
    std::sort(results.begin(), results.end());
    return results;
}

// Dizin boyutunu hesapla
uintmax_t directory_size(const fs::path& dir) {
    uintmax_t total = 0;
    for (const auto& entry : fs::recursive_directory_iterator(dir)) {
        if (entry.is_regular_file()) {
            total += entry.file_size();
        }
    }
    return total;
}

int main() {
    auto cpp_files = find_files(fs::current_path(), ".cpp");
    
    std::cout << "Bulunan .cpp dosyaları:\n";
    for (const auto& f : cpp_files) {
        auto rel = fs::relative(f, fs::current_path());
        std::cout << "  " << rel.string() << "\n";
    }
    
    std::cout << "Mevcut dizin boyutu: " 
              << directory_size(fs::current_path()) / 1024 << " KB\n";
    
    return 0;
}

💡 İpucu: fs::recursive_directory_iterator sembolik linkleri takip eder ve döngüsel linkler filesystem_error fırlatabilir. Güvenli kullanım için fs::directory_options::follow_directory_symlink yerine default davranışı tercih et ve try-catch ile sar.


Endianness Farkları

Endianness, çok baytlı verilerin bellekte hangi sırayla saklandığını belirler. İki ana tür vardır:

  • Little-endian: En düşük anlamlı bayt en düşük adreste (x86, x86-64, ARM — çoğu modern işlemci)

  • Big-endian: En yüksek anlamlı bayt en düşük adreste (network byte order, bazı PowerPC, SPARC)

Endianness, aynı makinede çalışan kodda genellikle sorun yaratmaz. Sorun, veri platformlar arasında taşındığında ortaya çıkar: dosya formatları, ağ protokolleri, binary serialization.

Endianness Tespit ve Dönüşüm

#include <iostream>
#include <cstdint>
#include <cstring>
#include <array>

// C++20 ile endianness tespiti
#if __cplusplus >= 202002L
#include <bit>

void check_endianness_cpp20() {
    if constexpr (std::endian::native == std::endian::little) {
        std::cout << "Little-endian sistem\n";
    } else if constexpr (std::endian::native == std::endian::big) {
        std::cout << "Big-endian sistem\n";
    } else {
        std::cout << "Mixed-endian sistem\n";
    }
}
#endif

// Manuel byte swap fonksiyonları
uint16_t swap_bytes_16(uint16_t value) {
    return (value >> 8) | (value << 8);
}

uint32_t swap_bytes_32(uint32_t value) {
    return ((value >> 24) & 0x000000FF) |
           ((value >> 8)  & 0x0000FF00) |
           ((value << 8)  & 0x00FF0000) |
           ((value << 24) & 0xFF000000);
}

// Network byte order dönüşüm (her platformda big-endian)
uint32_t to_network_order(uint32_t host_value) {
    #if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
        return swap_bytes_32(host_value);
    #else
        return host_value;  // Zaten big-endian
    #endif
}

uint32_t from_network_order(uint32_t net_value) {
    return to_network_order(net_value);  // Aynı işlem (swap kendi tersi)
}

int main() {
    uint32_t value = 0x01020304;
    
    // Bellekteki byte sırasını göster
    unsigned char* bytes = reinterpret_cast<unsigned char*>(&value);
    std::cout << "0x01020304 bellekte: ";
    for (int i = 0; i < 4; ++i) {
        std::cout << std::hex << static_cast<int>(bytes[i]) << " ";
    }
    std::cout << "\n";
    // Little-endian'da: 04 03 02 01
    // Big-endian'da:    01 02 03 04
    
    // Network order dönüşüm
    uint32_t net = to_network_order(value);
    std::cout << "Network order: 0x" << std::hex << net << "\n";
    
    return 0;
}

Ağ programlamasında standart htonl() / ntohl() fonksiyonları bu dönüşümü yapar. Ama kendi binary formatın varsa, endianness'i açıkça belgelemen ve dönüşüm fonksiyonları yazman gerekir.

Portable Binary Serialization

Farklı platformlar arasında binary veri taşırken, her zaman belirli bir endianness'e dönüştür (genellikle little-endian, çünkü çoğu modern işlemci öyle).

#include <iostream>
#include <fstream>
#include <cstdint>
#include <cstring>

// Portable yazma: her zaman little-endian
void write_le_u32(std::ostream& out, uint32_t value) {
    unsigned char bytes[4];
    bytes[0] = value & 0xFF;
    bytes[1] = (value >> 8) & 0xFF;
    bytes[2] = (value >> 16) & 0xFF;
    bytes[3] = (value >> 24) & 0xFF;
    out.write(reinterpret_cast<char*>(bytes), 4);
}

// Portable okuma: her zaman little-endian
uint32_t read_le_u32(std::istream& in) {
    unsigned char bytes[4];
    in.read(reinterpret_cast<char*>(bytes), 4);
    return static_cast<uint32_t>(bytes[0])
         | (static_cast<uint32_t>(bytes[1]) << 8)
         | (static_cast<uint32_t>(bytes[2]) << 16)
         | (static_cast<uint32_t>(bytes[3]) << 24);
}

struct GameSave {
    uint32_t score;
    uint32_t level;
    uint32_t playtime_seconds;
};

void save_game(const std::string& path, const GameSave& save) {
    std::ofstream file(path, std::ios::binary);
    write_le_u32(file, save.score);
    write_le_u32(file, save.level);
    write_le_u32(file, save.playtime_seconds);
}

GameSave load_game(const std::string& path) {
    std::ifstream file(path, std::ios::binary);
    GameSave save;
    save.score = read_le_u32(file);
    save.level = read_le_u32(file);
    save.playtime_seconds = read_le_u32(file);
    return save;
}

int main() {
    GameSave save{15000, 7, 3600};
    save_game("savegame.bin", save);
    
    auto loaded = load_game("savegame.bin");
    std::cout << "Score: " << loaded.score 
              << ", Level: " << loaded.level 
              << ", Time: " << loaded.playtime_seconds << "s\n";
    
    return 0;
}

Bu yaklaşımda struct'ı doğrudan fwrite() ile yazmak yerine her alanı ayrı ayrı serileştiriyoruz. Daha fazla kod ama her platformda aynı sonucu garanti ediyor.


Portable Tip Kullanımı

C++'ın temel tipleri (int, long, short) platformdan platforma farklı boyutlarda olabilir. int çoğu platformda 32-bit ama bu standardın garantisi değil. long ise Linux'ta 64-bit, Windows'ta 32-bit olabilir.

Fixed-Width Integer Types

<cstdint> header'ı, boyutu garanti edilen tipleri sunar:

#include <iostream>
#include <cstdint>
#include <climits>

int main() {
    // Boyutu garanti edilen tipler
    int8_t   a = -128;        // Tam 8 bit, signed
    uint8_t  b = 255;         // Tam 8 bit, unsigned
    int16_t  c = -32768;      // Tam 16 bit
    uint16_t d = 65535;       // Tam 16 bit
    int32_t  e = -2147483648; // Tam 32 bit
    uint32_t f = 4294967295;  // Tam 32 bit
    int64_t  g = 0;           // Tam 64 bit
    uint64_t h = 0;           // Tam 64 bit
    
    // Platform-bağımlı ama faydalı tipler
    size_t    size = sizeof(int);     // Dizi boyutu, index için
    ptrdiff_t diff = 0;               // Pointer farkı
    intptr_t  ptr  = 0;               // Pointer'ı tutabilecek integer
    
    // "En az" garanti tipler (minimum boyut, daha büyük olabilir)
    int_least32_t  fast = 42;   // En az 32 bit
    int_fast32_t   fast2 = 42;  // En az 32 bit, en hızlı
    
    std::cout << "int boyutu: " << sizeof(int) << " byte\n";
    std::cout << "long boyutu: " << sizeof(long) << " byte\n";
    std::cout << "size_t boyutu: " << sizeof(size_t) << " byte\n";
    std::cout << "int32_t boyutu: " << sizeof(int32_t) << " byte (garanti)\n";
    
    return 0;
}

Ne Zaman Hangi Tip?

DurumTipNeden
Dizi indexleme, boyutsize_tSTL uyumlu, yeterli büyüklük
Binary dosya formatıint32_t, uint64_t vb.Boyut garantisi şart
Ağ protokolüuint16_t, uint32_tBoyut + endianness kontrolü
Genel aritmetikintYeterli, okunabilir
Büyük sayılarint64_tPlatform farkı yok
Pointer aritmetiğiptrdiff_t, intptr_t32/64-bit uyumlu

⚠️ Dikkat: size_t unsigned'dır. Negatif kontrolü yapan döngülerde (i >= 0 gibi) sonsuz döngüye neden olabilir. Bu klasik bir tuzaktır.


CMake ile Cross-Platform Build

CMake, cross-platform build sistemlerinin fiili standardıdır. Platform algılama, koşullu derleme, bağımlılık yönetimi ve test entegrasyonunu tek bir yerde toplar.

Platform Algılama

cmake_minimum_required(VERSION 3.16)
project(CrossPlatformApp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ---- Platform Algılama ----
message(STATUS "Sistem: ${CMAKE_SYSTEM_NAME}")
message(STATUS "İşlemci: ${CMAKE_SYSTEM_PROCESSOR}")
message(STATUS "Derleyici: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")

# Platform-spesifik ayarlar
if(WIN32)
    message(STATUS "Windows build yapılandırılıyor...")
    add_definitions(-DUNICODE -D_UNICODE)
    add_definitions(-DNOMINMAX)           # Windows min/max macro'larını devre dışı bırak
    add_definitions(-D_CRT_SECURE_NO_WARNINGS)
    
elseif(APPLE)
    message(STATUS "macOS build yapılandırılıyor...")
    set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0")
    
elseif(UNIX)
    message(STATUS "Linux build yapılandırılıyor...")
    find_package(Threads REQUIRED)
endif()

# ---- Kaynak Dosyalar ----
set(COMMON_SOURCES
    src/main.cpp
    src/app.cpp
    src/config.cpp
)

# Platform-spesifik kaynak dosyalar
if(WIN32)
    list(APPEND PLATFORM_SOURCES src/platform/windows_impl.cpp)
elseif(APPLE)
    list(APPEND PLATFORM_SOURCES src/platform/macos_impl.cpp)
elseif(UNIX)
    list(APPEND PLATFORM_SOURCES src/platform/linux_impl.cpp)
endif()

add_executable(${PROJECT_NAME} ${COMMON_SOURCES} ${PLATFORM_SOURCES})

# ---- Derleyici Uyarıları (her platformda maximum) ----
if(MSVC)
    target_compile_options(${PROJECT_NAME} PRIVATE /W4 /WX)
else()
    target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()

# ---- Platform-spesifik linkler ----
if(WIN32)
    target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 userenv)
elseif(APPLE)
    find_library(COCOA_LIB Cocoa)
    target_link_libraries(${PROJECT_NAME} PRIVATE ${COCOA_LIB})
elseif(UNIX)
    target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads dl)
endif()

Bu CMakeLists.txt, üç platformda da çalışan bir yapı sağlar. WIN32, APPLE, UNIX gibi CMake değişkenleri otomatik olarak tanımlıdır.

Conditional Compilation ile Feature Toggle

# Opsiyonel özellikler
option(ENABLE_NETWORKING "Ağ desteğini etkinleştir" ON)
option(ENABLE_GUI "GUI desteğini etkinleştir" OFF)
option(ENABLE_PROFILING "Profiling desteğini etkinleştir" OFF)

# Feature macro'larını koda aktar
if(ENABLE_NETWORKING)
    target_compile_definitions(${PROJECT_NAME} PRIVATE HAS_NETWORKING=1)
    
    # Platform-spesifik ağ kütüphanesi
    if(WIN32)
        target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32)
    endif()
endif()

if(ENABLE_GUI)
    find_package(Qt6 COMPONENTS Widgets QUIET)
    if(Qt6_FOUND)
        target_compile_definitions(${PROJECT_NAME} PRIVATE HAS_GUI=1)
        target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets)
    else()
        message(WARNING "Qt6 bulunamadı, GUI devre dışı")
    endif()
endif()

if(ENABLE_PROFILING)
    target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_PROFILING=1)
    target_compile_options(${PROJECT_NAME} PRIVATE -pg)  # gprof
endif()

Kodda bu feature'ları kullanmak:

#include <iostream>

void initialize_app() {
    std::cout << "Uygulama başlatılıyor...\n";
    
    #ifdef HAS_NETWORKING
        std::cout << "Ağ modülü etkin\n";
        // init_networking();
    #endif
    
    #ifdef HAS_GUI
        std::cout << "GUI modülü etkin\n";
        // init_gui();
    #endif
    
    #ifdef ENABLE_PROFILING
        std::cout << "Profiling etkin\n";
        // start_profiler();
    #endif
}

int main() {
    initialize_app();
    return 0;
}

Build zamanında cmake -DENABLE_NETWORKING=ON -DENABLE_GUI=OFF .. gibi parametrelerle kontrol edilir.


CI/CD: Farklı Platformlarda Test

Kod tek platformda çalışıyor diye diğerlerinde çalışacağının garantisi yok. Matrix build stratejisi, aynı kodu birden fazla platformda ve derleyicide otomatik olarak test eder.

GitHub Actions Matrix Build

# .github/workflows/ci.yml
name: Cross-Platform CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-22.04, windows-2022, macos-13]
        build_type: [Debug, Release]
        include:
          - os: ubuntu-22.04
            cc: gcc-12
            cxx: g++-12
          - os: macos-13
            cc: clang
            cxx: clang++
          - os: windows-2022
            cc: cl
            cxx: cl

    runs-on: ${{ matrix.os }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure CMake
        env:
          CC: ${{ matrix.cc }}
          CXX: ${{ matrix.cxx }}
        run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
      
      - name: Build
        run: cmake --build build --config ${{ matrix.build_type }} -j$(nproc 2>/dev/null || echo 2)
      
      - name: Test
        run: ctest --test-dir build --output-on-failure -C ${{ matrix.build_type }}

Bu konfigürasyon, her push'ta kodu 6 farklı kombinasyonda (3 OS × 2 build type) test eder. fail-fast: false önemli: bir platformdaki hata diğerlerinin testini engellemez, böylece tek seferde tüm sorunları görürsün.

Sık Karşılaşılan Cross-Platform CI Sorunları

SorunPlatformÇözüm
\r\n vs \nWindows ↔ Linux.gitattributes ile line ending kontrolü
Case-insensitive dosya sistemimacOS/Windows#include path'lerinde büyük-küçük harf tutarlılığı
/ vs \ yol ayracıWindowsstd::filesystem::path kullan, string birleştirme yapma
long boyutuLinux (64-bit) vs Windows (32-bit)int64_t kullan
__attribute__ vs __declspecGCC/Clang vs MSVCOrtak makrolar tanımla

Gerçek Dünya Örneği: Cross-Platform Log Dizini

Öğrendiklerimizi birleştiren bir fonksiyon: platform algılama, std::filesystem, portable tipler.

#include <iostream>
#include <filesystem>
#include <cstdlib>
#include <cstdint>

namespace fs = std::filesystem;

// Platform-bağımsız log dizini belirleme
fs::path get_log_directory(const std::string& app_name) {
    fs::path base;
    
    #if defined(_WIN32)
        const char* appdata = std::getenv("LOCALAPPDATA");
        base = appdata ? fs::path(appdata) / "logs" : fs::path("C:/ProgramData/logs");
    #elif defined(__APPLE__)
        const char* home = std::getenv("HOME");
        base = home ? fs::path(home) / "Library" / "Logs" : fs::path("/tmp/logs");
    #elif defined(__linux__)
        const char* home = std::getenv("HOME");
        base = home ? fs::path(home) / ".local" / "share" / "logs" : fs::path("/tmp/logs");
    #else
        base = fs::path("/tmp/logs");
    #endif
    
    auto log_dir = base / app_name;
    fs::create_directories(log_dir);  // Platform-bağımsız dizin oluşturma
    return log_dir;
}

int main() {
    auto dir = get_log_directory("my_app");
    std::cout << "Log dizini: " << dir << "\n";
    
    // Tarih bazlı log dosyası
    auto log_file = dir / "2024-01-15.log";
    std::cout << "Log dosyası: " << log_file << "\n";
    std::cout << "Dosya adı: " << log_file.filename() << "\n";
    std::cout << "Uzantı: " << log_file.extension() << "\n";
    
    return 0;
}

Platform-spesifik tek yer get_log_directory() fonksiyonundaki #if bloğu. Geri kalan her şey — fs::path, create_directories, dosya işlemleri — tamamen platform bağımsız. Cross-platform tasarımın özü: farklılıkları küçük, izole noktalarda topla, geri kalanı saf C++ ile yaz.

💡 İpucu: Windows <windows.h> header'ında ERROR, min, max gibi birçok makro tanımlıdır. Cross-platform kodda NOMINMAX tanımlamak min/max çakışmasını çözer ama tüm makroları engelleyemezsiniz. Enum değerlerinde ERROR yerine ERR gibi alternatifler kullanmak güvenlidir.


Özet

  • Platform bağımsız kod, daha geniş kullanıcı kitlesi ve daha temiz mimari demektir — #ifdef spagettisinden kaçının, farklılıkları soyutlama katmanında izole edin.

  • Preprocessor makrolar (_WIN32, __linux__, __APPLE__) platform ve derleyici algılama için temel araçtır — ama kullanımı minimum tutulmalı, iş mantığına sızmamalıdır.

  • `std::filesystem` (C++17) dosya yolu, dizin oluşturma, dosya listeleme gibi işlemleri tamamen platform bağımsız hale getirir — artık platform-spesifik API'ye gerek yok.

  • Fixed-width tipler (int32_t, uint64_t, size_t) boyut belirsizliğini ortadan kaldırır — binary format ve ağ protokollerinde olmazsa olmazdır.

  • CMake, cross-platform build'in standart aracıdır — platform algılama, koşullu derleme, feature toggle'lar ve test entegrasyonunu tek çatıda toplar.

  • CI/CD matrix build, kodun tüm hedef platformlarda çalıştığını her commit'te doğrular — fail-fast: false ile tüm sorunları tek seferde görün.