C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
분야별 포럼
C++빌더
델파이
파이어몽키
C/C++
프리파스칼
파이어버드
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

자유게시판
세상 살아가는 이야기들을 나누는 사랑방입니다.
[28886] 아래 컴파일 에러는 엠바에서 템플릿코드를 엉터리로 구현해 놨기 때문.
빌더(TWx) [builder] 5208 읽음    2021-03-06 08:12
아루스 님이 쓰신 글 :
: Delphi의 Format은 string, array of const 를 인자로 받습니다.
: 그래서 변수들을 [] 로 감싸서 array 표현을 한거죠.
:
: C++ 에서는 string, TVarRec*, int 의 3개의 인자를 받게 되어있는데,
: TVarRec*, int 를 처리해주는 ARRAYOFCONST(( )) 매크로가 있습니다.
:
: Caption = Format("%s", ARRAYOFCONST((wstr))); // ok
: Caption = Format("%s - %d", ARRAYOFCONST((wstr, i))); // ok
:
: 항상 ARRAYOFCONST를 쓰면 불편해서인지 가변 인자의 Format이 System.SysUtils.hpp 에 있습니다.
: 어느 버전부터 있는지는 모르겠습니다만, 1번과 4번의 경우는 이 함수에서 처리가 되어서 에러가 나지 않았습니다.
:
: 가변 인자 함수로 처리되려면 is_TVarRec_compat 조건을 만족해야 하는데, wchar_t * 는 해당하지 않습니다.
: 그래서 매칭되는 함수를 찾지 못합니다.
:
: System.SysUtils.hpp 를 열어서 is_TVarRect_compat 을 찾아보시면 가변 Format에 허용되는 타입을 찾아 볼 수 있습니다.
: 10.4.2 기준으로 void, char, int, unsigned, long, unsigned long, double(win64, ios, android 허용), long double, ShortString, UnicodeString, Currency, Variant, WideString, long long, unsigned long long 이 있습니다.
:
: 가변 Format 코드를 복사해서 Enable 조건을 제거해보면 아래의 코드도 잘 동작하긴하는데,
: char*, wchar_t* 의 경우는 특정 조건(null-terminated)을 만족하지 않으면 안전하지 않아서 허용하지 않은건가 하고 추측을 해봅니다;
:
: 짜증 님이 쓰신 글 :
: : 시드니 10.4.2 테스트중인데여
: :
: : procedure TForm2.Button1Click(Sender: TObject);
: : var
: :   wstr: PWideChar;
: :   i: Integer;
: : begin
: :   wstr := 'wstr';
: :   i := 888;
: :   Caption := Format('%s', [wstr]); // 정상
: :   Caption := Format('%d - %s', [i, wstr]);  // 정상
: :   Caption := Format('%s - %d', [wstr, i]);  // 정상
: : end;
: :
: :
: : void __fastcall TForm1::Button1Click(TObject *Sender)
: : {
: :   String str = "str";
: :   int i = 888;
: :   const wchar_t *wstr = L"wstr";
: :   Caption = Format("%d", i); // 정상
: :   Caption = Format("%s", wstr); // 컴파일 에러
: :   Caption = Format("%s - %d", wstr, i); // 컴파일 에러
: :   Caption = Format("%d - %s", i, wstr); // 정상
: : }
: :
: : 델파이는 에러 없이 컴파일되는데
: : 빌더로 하면 위와 같은 에러가 나거든여
: :
: : 첫번째와 네번째는 에러없이 컴파일되면서
: : 두번째 세번째는 컴파일 에러가 나는데
: : 도무지 이해가 안되네여
: :
: : 10.4.1로 해봐도 같은 증상입니다
: :



답변:



위 코드에서 컴파일 에러가 발생하는 것은...
"char*, wchar_t* 인 경우 특정 조건(null-terminated)을 만족하지 않으면 안전하지 않아서 허용하지 않은"게 아니고

엠바에서 template 코드를 엉터리로 구현해 놨기 때문 입니다.

윈도우즈 OS 에서 모든 API가 스트링 리터럴 타입으로 char*, wchar_t* 타입을 쓰고 있는데 허용하지 않는 건
말이 안되죠.


내용을 파악하기 위해선 파스칼 코드에 대한 이해가 먼저 선행돼야 할 것 같아서
델파이 컴파일러가 어떤 식으로 Format(......) 부분을 컴파일 하는지 부터 살펴보도록 합시다.


var
  wstr: PWideChar;
begin
  wstr := 'abc';
  Format('%s', [wstr]);

델파이 컴파일러가 위의 Format() 구문에서 [wstr]을 만나면...
다음과 같은 set 타입도 아니고

  if not (key in [v1, v2]) then
    key := #0;

아래와 같은 프로토타입으로 선언되어 있기 때문에

function Format(const Format: string; const Args: array of const): string;

소스코드 파싱시... [wstr] 부분을 'array of const'로 취급하게 되고.
'const Args: array of const'에서 선두의 const 는 파스칼 언어에서 주소 처럼 취급되는데
문법을 모호하게 만드는 안좋은 케이스임.

[wstr] 부분을 'array of const'로 취급하므로 컴파일러는 스택에 A라는 영역을 Reserve해서
인수 갯수 만큼의 static 배열을 temporary로 할당하게 됍니다. A는 뒤에서 다시 후술 함.

그리고...
스택에 temporary로 할당된(A) 영역에 wstr의 주소값과 0x0A 값을 넣고

다음과 같은 식으로 파라미터를 구성해서 Format() 함수를 호출하는 코드를 생성 합니다.
var
  A; // temporary
  A.value := wstr;
  A.type := 0x0A;
  Format('%s', Pointer(A), 0); // 0 = '[]인수의 갯수 - 1' // zero base 상한 값


여기서 컴파일러가 emit 한 0x0A 값은 PWideChar 타입임을 나타내는 const 상수 값임.



외견상으로는 2개의 파라미터가 사용되는 것처럼 보이지만

Format(const String& fmt, void*, int nItems);

와 같은 식으로

실제적으로 델파이 컴파일러는 3개의 파라미터를 사용하는 코드를 생성하므로

델파이로 컴파일 되어있는 Format()함수를 C++빌더에서 호출해서 사용하기 위해선
파라미터를 맞추어 줘야 할 필요가 있고, 기존에 사용했던 방식이 ARRAYOFCONST 매크로 였고
이 매크로는 A의 주소와 A(Items) -1 한 값을 파라미터로 사용하도록 확장해 주죠.



델파이 파스칼 언어는 다른 언어와 달리

foo(const char* fmt, ...);

와 같은 가변인수를 지원하지 않기 때문에 위와 같이 배열을 이용하는 꼼수를 대신 사용하고 있는 겁니다.


앞서 언급했던 A라는 데이타 구조와 0x0A 상수값은 델파이 컴파일러 내부에서 정의해 놓고 있는 Internal 타입이고
컴파일러 내부에서 정의되어 있는 이 Internale 타입은 델파이 RTL 에 다음과 같이 기술되어 있음.

{ TVarRec.VType values }

  vtInteger       = 0;
  vtBoolean       = 1;
  vtChar          = 2;
  vtExtended      = 3;
  vtString        = 4{$IFDEF NEXTGEN} deprecated 'Type not supported' {$ENDIF NEXTGEN};
  vtPointer       = 5;
  vtPChar         = 6;
  vtObject        = 7;
  vtClass         = 8;
  vtWideChar      = 9;
  vtPWideChar     = 10;
  vtAnsiString    = 11;
  vtCurrency      = 12;
  vtVariant       = 13;
  vtInterface     = 14;
  vtWideString    = 15;
  vtInt64         = 16;
  vtUnicodeString = 17;



var
  wstr: PWideChar;
begin
  wstr := 'abc';
  Format('%s', [wstr]);

위 구문에서 컴파일러가 temporary 스택에 넣었던 0x0A 값은(십진수로 10)
PWideChar 타입임을 나타내는 컴파일러 Internal 상수 값임.

그리고 위에서 언급했던 A라는 데이타 구조는 컴파일러 내부에서 정의되어있는 데이타 구조이고
RTL 에 다음과 같이 기술되어 있죠.

  PVarRec = ^TVarRec;
  TVarRec = record { do not pack this record; it is compiler-generated }
    case Integer of
      0: (case Byte of
            vtInteger:       (VInteger: Integer);
            vtBoolean:       (VBoolean: Boolean);
            vtChar:          (VChar: _AnsiChr);
            vtExtended:      (VExtended: PExtended);
{$IFNDEF NEXTGEN}
            vtString:        (VString: _PShortStr);
{$ENDIF !NEXTGEN}
            vtPointer:       (VPointer: Pointer);
            vtPChar:         (VPChar: _PAnsiChr);
{$IFDEF AUTOREFCOUNT}
            vtObject:        (VObject: Pointer);
{$ELSE}
            vtObject:        (VObject: TObject);
{$ENDIF}
            vtClass:         (VClass: TClass);
            vtWideChar:      (VWideChar: WideChar);
            vtPWideChar:     (VPWideChar: PWideChar);
            vtAnsiString:    (VAnsiString: Pointer);
            vtCurrency:      (VCurrency: PCurrency);
            vtVariant:       (VVariant: PVariant);
            vtInterface:     (VInterface: Pointer);
            vtWideString:    (VWideString: Pointer);
            vtInt64:         (VInt64: PInt64);
            vtUnicodeString: (VUnicodeString: Pointer);
         );
      1: (_Reserved1: NativeInt;
          VType:      Byte;
         );
  end;


델파이 파스칼은 컴파일러와 RTL이 서로 종속되어 있는 취약한 구조를 갖고 있음.


위와 같은 이유로...
델파이로 컴파일 되어있는 Format()함수의 실제 프로토타입은 다음과 같게 됍니다.

String __fastcall Format(const String s, const TVarRec *a, const int a_High);



델파이 파스칼 언어는 또 다른 문제도 갖고있는데...

var
  wstr: PWideChar;
  astr: PAnsiChar;
begin
  wstr := 'abc';
  astr := 'abc';
  Format('%s', [wstr]);
  Format('%s', [astr]);

C++언어에선...

"%s, %ls, %ws" 와 같이 스트링 리터럴 타입이 유니코드인지 앤시코드인지 구분하는 방법을 갖고있으나
델파이 파스칼 언어는 '%s' 하나 밖에 지정할 수 없어서 인수의 타입을 구분할 방법이 필요한데

[wstr]에서 0x0A라는 상수값을 컴파일러가 생성했다면
[astr]에선 컴파일러가 0x06 값을 생성 합니다.

여기서 0x06 값은 PAnsiChar 타입임을 나타내는 컴파일러 내부에서 정의되어 있는 상수값이고
RTL 에 vtPChar = 6; 으로 기술되어 있음.

모든 인수에 대해서...
컴파일러가 타입을 구분하는 상수값을 일일히 emit해서 인수 갯수 만큼 temporay 배열을 생성해야 하는데.
컴파일러를 상당히 멍청하게 만들어 놨죠.

델파이 파스칼은... 런타임 비용 댓가를 크게 치러야 하는 취약한 구조를 갖고있고
랭귀지 스펙이 간단해서 초보자들이 쉽게 사용할 수 있는 언어에 불과 함.

특히...

델파이 Interface는 RTL Internal 함수들을 대거 호출하는 코드를 컴파일러가 생성해서 실행해야 하기 때문에
IDE 패치가 필요할 때... 델파이 Interface로 구현되어 있는 Tools API는 거들떠 보지도 않게 됍니다.
IDE 내부구조를 직접 패치해서 사용하는 게 불필요한 런타임 비용 없이 더 효율적으로 처리할 수 있어서.




자...

이제 컴파일 에러의 직접적인 원인인 template 버그 문제를 짚어 봅시다.


델파이로 컴파일 되어있는 Format() 함수를 매크로 없이 가변인수 형식으로 호출해서 사용할 수 있게 하기위해서
다음과 같이 variadic template 테크닉을 이용해서 구현해 놨는데...

<System.SysUtils.hpp>
#if (__cplusplus >= 201103L) && !defined(SYSTEM_SYSUTILS_NO_VARIADIC_FORMAT)
  namespace internal {
    // NOTE: Would be better to use 'std::' but can't force 'type_traits' in
    //       'System.SysUtils.hpp'. Hence, home-made traits
    template< bool B, class T = void >
    struct only_if {};

    template< class T >
    struct only_if< true, T > { typedef T type; };

    template< class T, T v >
    struct integral_constant {
      static constexpr T value = v;
      using value_type = T;
    };

    template< typename T, typename Y >
    struct is_same {
      enum { value = 0 };
    };

    template< typename T >
    struct is_same< T, T > {
      enum { value = 1 };
    };

    template< std::size_t N, typename T = void, typename... types >
    struct GetType
    {
      using type = typename GetType< N - 1, types... >::type;
    };

    template< typename T, typename... types >
    struct GetType< 0, T, types... >
    {
      using type = T;
    };
    template < typename T >
    using is_TVarRec_compat = std::integral_constant< bool,
                      is_same< T, void >::value ||
                      is_same< T, char >::value ||
                      is_same< T, int >::value ||
                      is_same< T, unsigned >::value ||
                      is_same< T, long >::value ||
                      is_same< T, unsigned long >::value ||
#if defined(_WIN64) || defined(TARGET_OS_IPHONE) || defined(__ANDROID__)
                      is_same< T, double >::value ||
#endif
                      is_same< T, long double >::value ||
                      is_same< T, System::ShortString >::value ||
                      is_same< T, System::UnicodeString >::value ||
                      is_same< T, System::Currency >::value ||
                      is_same< T, System::Variant >::value ||
                      is_same< T, System::WideString >::value ||
                      is_same< T, long long >::value ||
                      is_same< T, unsigned long long >::value
                                                    >;
  }


  // Variadic version of Format. Supports the more natural syntax of
  //   Format("A string='%s', An int='%d'", str, i);
  template < typename... Args,
            class Enable = typename internal::only_if<
                             internal::is_TVarRec_compat<
                               typename internal::GetType< 0, Args...> ::type >::value >::type >
  System::String Format(const char *fmt, const Args&... args) {
    System::TVarRec arg_list[] = {args...};
    return Format(System::String(fmt), arg_list, sizeof...(Args)-1);
  }

  template < typename... Args,
            class Enable = typename internal::only_if< 
                             internal::is_TVarRec_compat<
                               typename internal::GetType< 0, Args... >::type >::value >::type >
  System::String Format(const System::WideChar *fmt, const Args&... args) {
    System::TVarRec arg_list[] = {args...};
    return Format(System::String(fmt), arg_list, sizeof...(Args)-1);
  }
#endif


코드가 제법 긴 편인데...
여기서... 실제로 생성되는 코드는

'return Format(System::String(fmt), arg_list, sizeof...(Args)-1);'

이 부분만 컴파일러에 의해서 런타임 중에 실행될 실제 코드로 생성되고
다른 부분들은 어떤 코드도 생성되지 않고 컴파일 타임 시에만
인수들의 타입 정보를 체크하기 위해서만 이용 됍니다. (런타임 비용 Zero)

이런 걸 템플릿 메타 프로그래밍이라고 하는데...
다른 언어와 달리 C++언어 만 갖고있는 강력한 파워죠.

C++ 중급 이상이 아니면 이해하기 힘들거 같고, 또 이걸 설명할려면 A4 용지로 수십 장은 작성해야 할 것 같아
여기서 구체적인 설명은 생략 합니다.

C++20 template concept 테크닉을 이용하면 간단하게 constraint를 구현할 수 있는데
엠바 컴파일러는 C++20을 지원하지 않음.



위에서 보는 바와 같이...
char*와 wchar_t* 타입은 is_TVarRec_compat 에서 별도로 정의되어 있지 않죠.

그렇다고 이게 char*와 wchar_t* 타입을 허용하지 않기 위한 목적은 아니고.


String wstr = "abc";
String wstr = L"abc";

String wstr("abc");
String wstr(L"abc");

위와 같이 String 타입은...

char*와 wchar_t* 타입에 대한 constructor와 assignment operator가 String 클래스에서
정의되어 있기 때문에 String 타입 자체로 char*와 wchar_t* 타입을 다 사용할 수 있고

이런 점에 착안해서 template constraint를 저런 식으로 구현해 놓은 건데...


겉멋을 잔뜩 부려서 template 메타 프로그래밍 테크닉을 이용해서 똥폼을 잡았지만 헛발질 해놓은 격이라고나 할까요.


언급한 대로...

char*와 wchar_t* 타입에 대한 constructor와 assignment operator가
String 클래스에서 정의되어 있고...

char*와 wchar_t* 이외의 타입들 또한 TVarRec에 constructor와 assignment operator로 정의 되어 있기 때문에
is_same<>으로 주루룩 선언되어있는 constraint는 다 쓸데 없는 것들에 불과한 거고

다음과 같이 is_assignable<> 한줄로 처리할 수 있는 건데.

겉멋만 부려서 아마추어 같이 template 코드를 엉터리로 구현해 놔서 이런 문제가 발생하게 됀 겁니다.


80 라인의 코드로 구현되어 있는 문제의 코드는
다음과 같이 20 라인의 코드로 줄여서 버그픽스 됄 수 있지요.

<Unit1.cpp>
using namespace std;

// 버그 픽스된 코드
#if 1 // bug fix...
namespace _internal {
  template < bool B >
  using only_if = typename std::enable_if < B >::type;

  template< std::size_t N, class T = void, class... A >
  struct GetType {
    using type = typename GetType< N - 1, A... >::type;
  };

  template< class T, class... A >
  struct GetType< 0, T, A... > {
    using type = T;
  };
}

template < class T, class... A, class Enable = typename _internal::only_if<
          is_assignable< TVarRec, typename _internal::GetType< 0, A... >::type >::value> >
String Format(T&& t, A&&... a)
{
  return System::Sysutils::Format(t, (TVarRec[]){a...}, sizeof...(A)-1);
}
#endif



// 테스트 코드
void __fastcall TForm1::Button1Click(TObject *Sender)
{
  String vString = "vString";
  int i = 888;
  void* pVoid = &i;
  const wchar_t* pWStr = L"pWStr";
  const char* pStr = "pStr";

  Memo1->Lines->Add(Format("%d", i));
  Memo1->Lines->Add(Format("%d - %s", i, pStr));
  Memo1->Lines->Add(Format("%s", vString));
  Memo1->Lines->Add(Format("%s - %d", pWStr, i));
  Memo1->Lines->Add(Format("%d - %s", i, pWStr));
  Memo1->Lines->Add(Format("%s - %d", pStr, i));

  AnsiString vAnsiString = "vAnsiString";
  Memo1->Lines->Add(Format(L"%p - %s", pVoid, vAnsiString));
  Memo1->Lines->Add(Format(L"%p - %d", pVoid, i));

  std::string s = "std::string";
  std::wstring ws = L"std::wstring";
  Memo1->Lines->Add(Format("%s", s.c_str()));
  Memo1->Lines->Add(Format("%s", ws.c_str()));
  Memo1->Lines->Add(Format("%s", AnsiString("AnsiSring")));
  Memo1->Lines->Add(Format("%s", ShortString("ShortSring")));
}
//---------------------------------------------------------------------------








IDE, Compiler, Debugger, LSP, RTL, VCL 등등...

디테일하게 들여다 보면 엠바툴은 문제가 너무 많아요. 아마추어가 만들어 놓은 것 처럼.




+ -

관련 글 리스트
28886 아래 컴파일 에러는 엠바에서 템플릿코드를 엉터리로 구현해 놨기 때문. 빌더(TWx) 5208 2021/03/06
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.