21 Ağustos 2020 Cuma

One Definition Rule - ODR

Giriş
One Definition Rule yani ODR, bir "şeyin" tek bir yerde tek bir defa tanımlı olmasını gerektirir. Bu "şey" sınıf, metod vs olabilir. Yani bir kod parçasıdır. 

ODR C++'ın gıcık konularından birisi. Bu yüzden ODR kullanımı, ihlalleri ile ilgili maddeleri bu yazıda toplamaya çalıştım.

ODR Ne İçin Lazımdır?
ODR olmasaydı geriye doğru referanslar çalışmazdı. Şu kod derlenmezdi.
// Does not compile
struct tree {
  struct node *root;
};
struct node {
  struct node *left;
  struct node *right;
  struct tree *owner;
};
ODR Varsa Ne Olur?
ODR ihlali varsa linker'ın nasıl çalışacağı belirsiz. Açıklaması şöyle.
The linker assumes that the ODR has NOT been violated and is therefor free to use ANY of the instantiations ...
ODR İhlalleri
Şimdi bazı ODR ihlali durumlarına bakalım

1. Aynı değişkenin iki farklı yerde tanımlanması
Açıklaması şöyle.
3.2 One-definition rule
4 Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement (6.4.1); no diagnostic required.
Örnek
Şu kod ODR kuralını ihlal ettiği için derlenmez
int x;
int x;
Şu kod ODR kuralını ihlal etmez.
// a declaration with an extern storage class specifier and without an initializer
extern int x;
extern int x;
Örnek
ODR derlemeye dahil olmayan kodlar için haliyle işlemez. Elimizde şöyle bir kod olsun
#include <iostream>

extern int i; // Only declaration

int func() 
{
    if constexpr (true)
        return 0;
    else if (i)
        return i;
    else
        return -1;
}

int main() 
{
    int ret = func();
    std::cout<<"Ret : "<<ret<<std::endl;
}
constexpr yüzünden alttaki i'yi kullanan kod derlenmediği için ODR kuralı işlemez.

2.Aynı metodun iki farklı yerde vücut bulması
Açıklaması şöyle.
"In the entire program, an object or non-inline function cannot have more than one definition"
Aynı metodun iki kere vücut bulması ile iki kere tanımlanması (declare) farklı şeylerdir. Şu kod ODR kuralını ihlal etmez.
int add(int, int);
int add(int, int);
Örnek - normal + inline metod
file1.cpp şöyle olsun
int func1 (void) { return 5; }
file2.cpp şöyle olsun
inline int func1 (void) { return 5; }
Bu kod her iki metod aynı olmasına rağmen, file2.cpp içindeki inline olduğu için derlenir.

Örnek - iki inline metodu
inline metod iki farklı yerde tanımlanabilir ve ODR kuralını ihlal etmez. Eğer inline metod iki farklı yerde tanımlı ise ise aynı koda sahip olmaları beklenir. inline specifier yazısına bakabilirsiniz.

3.Aynı İki Template Metodun İki Farklı Yerde Vücut Bulması
Örnek
Elimizde a.cpp olsun
#include <iostream>

double bar();

template <typename T>
T foobar(T t) {
  return t;
}

int main() {
  std::cout << "foobar called from b.cpp: " << bar() << '\n';
  std::cout << "foobar called from a.cpp: " << foobar(1.) << '\n';
}
Elimizde b.cpp olsun
template <typename T>
T foobar(T t) {
  return t + 1.;
}

double bar() {
  return foobar(1.);
}
Bu kod derlense bile ODR kuralını ihlal eder. Yani undefined behavior (UB) verir.
Örnek
Elimizde şöyle bir kod olsun
#pragma once

#include <iostream>

template <int>
void func() { std::cerr << "default impl\n"; } // normally no impl here
func metodunu farklı template parametreleri ile farklı bir dosyada daha gerçekleştirelim
#include "func.h"

template <>
void func<1>()
{
  std::cerr << "special 1\n";
}

template <>
void func<2>()
{
  std::cerr << "special 2\n";
}
Ve ilk metodu çağıralım
#include "func.h"

int main(void)
{
  func<1>();
  func<2>();

  return 0;
}
Bu kod derlense bile ODR kuralını ihlal eder. Yani undefined behavior (UB) verir. Optimizasyon olmadan derleyip çalıştırırsak çıktı olarak şunu alırız
special 1
special 2
Optimizasyon ile derleyip çalıştırırsak çıktı olarak şunu alırız
default impl
default impl
Çözüm
Açıklaması şöyle.
The solution to this is to forward declare the specializations in your header:
Şöyle yaparız.
template<> void func<1>();
template<> void func<2>();
4.Aynı metodun iki farklı yerde iki farklı isim alanı içinde vücut bulması
Açıklaması şöyle. Hiding veya shadowing deniliyor.
A name can be hidden by an explicit declaration of that same name in a nested declarative region or derived class
Şöyle yaparız.
#include <cmath>

double log(double) {return 1.0;}
int main() {
  log(1.0);
}
5. Aynı sınıfın iki farklı yerde Anonymous/Unnamed isim alanı içinde sınıf tanımlanması
Anoymous Namespace yazısına bakabilirsiniz. Açıklaması şöyle
 "Use of unnamed namespaces in header files can easily cause violations of the C++ One Definition Rule (ODR)."
Örnek
Şu kod aynı sınıfı unnamed isim alanı içinde iki farklı yerde tanımladığı için ODR kuralını ihlal eder.
//A.cpp
class X
{
};

//B.cpp
class X
{
  int i;
};
Örnek
pimpl için şöyle yapılmış olsun. Impl önce forward declare edilir. Daha sonra Foo.h içinde kullanılır.
namespace { struct Impl; }

class Foo
{
public:
  Foo();
  ~Foo();

  void Bar(int n);
  /* ... */

private:
  std::unique_ptr<Impl> _impl;
};
Foo.h'ı dahil eden her sınıf şu problemle karşılaşır
If this occurs in a header file that is used by multiple translation units, then I think it is an ODR violation. Each anonymous namespace is different in each translation unit, therefore class Foo will not have a consistent definition across all translation units.
Doğrusu Impl'in Foo içinde tanımlanmasıydı. Şöyle yaparız.
class Foo
{
struct Impl;
public:
  ...
};
Tam bir PImpl .h dosyası şöyledir.
// header file:
class Cat {
private:
  class CatImpl;  // Not defined here
  CatImpl *cat_;  // Handle

public:
  Cat();            // Constructor
  ~Cat();           // Destructor
  // Other operations...
   Purr();
};
Tam bir PImpl cpp dosyası şöyledir.
// CPP file:
#include "cat.h"

class Cat::CatImpl {
  Purr();
  ...     // The actual implementation can be anything
};

Cat::Cat() {
  cat_ = new CatImpl;
}

Cat::~Cat() {
  delete cat_;
}

Cat::Purr(){ cat_->Purr(); }
CatImpl::Purr(){
  printf("purrrrrr");
}
Örnek
Şu kod sanırım problemsiz. MyClass.h dosyasında şöyle yaparız.
#if !defined(MYCLASS_PUBLIC_H_)
#define MYCLASS_PUBLIC_H_

struct MyClassImpl;
class MyClass {
  MyClassImpl * pImpl;
public:
  void SomeOperation();
};
#endif
MyClassImpl.h dosyasında şöyle yaparız.
#if !defined(MYCLASS_IMPL_H_)
#define MYCLASS_IMPL_H_
#include <private_type.h>
#include "MyClass.h"
struct MyClassImpl
{
  void Operation();

private:
  SomePrivateType member;
};
#endif
MyClassImpl.cpp dosyasında şöyle yaparız.
#include "MyClassImpl.h"
void MyClass::SomeOperation()
{
  pImpl->Operation();
}

void MyClassImpl::Operation()
{
  // do something with 'member'
}
Örnek
Şu kod da hatalıdır.
namespace {
double const pi = 3.14159;
}

inline double twoPiR( double r ) { return 2.0 * pi * r; }
Örnek
Şu kod da hatalıdır.
namespace {
  int x;
}

namespace{
  int x;
}


Hiç yorum yok:

Yorum Gönder