เหตุใดฉันจึงสามารถกำหนดโครงสร้างและคลาสภายในฟังก์ชันใน C ++ ได้


92

ฉันเพิ่งทำสิ่งนี้ผิดพลาดใน C ++ และมันก็ใช้ได้ ทำไมฉันถึงทำได้

int main(int argc, char** argv) {
    struct MyStruct
    {
      int somevalue;
    };

    MyStruct s;
    s.somevalue = 5;
}

หลังจากทำสิ่งนี้แล้วฉันก็จำได้ว่าอ่านเคล็ดลับนี้ที่ไหนสักแห่งเมื่อนานมาแล้วว่าเป็นเครื่องมือการเขียนโปรแกรมที่ใช้งานได้ยากสำหรับ C ++ แต่ฉันจำไม่ได้ว่าทำไมมันถึงถูกต้องหรือฉันอ่านที่ไหน

ยินดีต้อนรับคำตอบสำหรับคำถามใด ๆ !

หมายเหตุ: แม้ว่าเมื่อเขียนคำถามฉันไม่ได้รับการอ้างอิงใด ๆ เกี่ยวกับคำถามนี้แต่แถบด้านข้างปัจจุบันก็ชี้ให้เห็นดังนั้นฉันจะใส่ไว้ที่นี่เพื่ออ้างอิงไม่ว่าคำถามจะแตกต่างกัน แต่อาจมีประโยชน์


คำตอบ:


72

[แก้ไข 18/4/2556]: โชคดีที่ข้อ จำกัด ที่กล่าวถึงด้านล่างถูกยกออกใน C ++ 11 ดังนั้นคลาสที่กำหนดไว้ในเครื่องจึงมีประโยชน์ในที่สุด! ขอบคุณผู้แสดงความคิดเห็น bamboon

ความสามารถในการกำหนดคลาสแบบโลคัลจะทำให้การสร้าง functors แบบกำหนดเอง (คลาสที่มีoperator()()ฟังก์ชันเปรียบเทียบสำหรับการส่งผ่านไปยังstd::sort()หรือ "loop body" ที่จะใช้ด้วยstd::for_each()) สะดวกกว่ามาก

น่าเสียดายที่ C ++ ห้ามไม่ให้ใช้คลาสที่กำหนดในเครื่องด้วยเทมเพลตเนื่องจากไม่มีการเชื่อมโยง เนื่องจากแอปพลิเคชัน functors ส่วนใหญ่เกี่ยวข้องกับประเภทเทมเพลตที่เทมเพลตในประเภท functor จึงไม่สามารถใช้คลาสที่กำหนดในเครื่องได้คุณต้องกำหนดไว้นอกฟังก์ชัน :(

[แก้ไข 1/11/2009]

คำพูดที่เกี่ยวข้องจากมาตรฐานคือ:

14.3.1 / 2:ห้ามใช้ประเภทโลคัลประเภทที่ไม่มีการเชื่อมโยงประเภทที่ไม่มีชื่อหรือประเภทที่ผสมจากประเภทใดประเภทนี้เป็นอาร์กิวเมนต์แม่แบบสำหรับพารามิเตอร์ชนิดเทมเพลต


2
แม้ว่าในเชิงประจักษ์สิ่งนี้ดูเหมือนจะใช้ได้กับ MSVC ++ 8 (แต่ไม่ใช่กับ g ++)
j_random_hacker

Im ใช้ GCC 4.3.3 และมันดูเหมือนว่าจะทำงานที่นั่นpastebin.com/f65b876b คุณมีข้อมูลอ้างอิงว่ามาตรฐานห้ามไว้ที่ไหน? สำหรับฉันแล้วดูเหมือนว่ามันสามารถสร้างอินสแตนซ์ได้อย่างง่ายดายในเวลาที่ใช้งาน
แมวสกุล

@Catskul: 14.3.1 / 2: "ประเภทโลคัลประเภทที่ไม่มีการเชื่อมโยงประเภทที่ไม่มีชื่อหรือประเภทที่ผสมจากประเภทเหล่านี้จะไม่ถูกใช้เป็นอาร์กิวเมนต์แม่แบบสำหรับพารามิเตอร์ type-template" ฉันเดาว่าเหตุผลก็คือชั้นเรียนในท้องถิ่นจะต้องมีข้อมูลอีกจำนวนมากเพื่อผลักดันให้เป็นชื่อที่แหลกเหลว แต่ฉันไม่รู้ว่าแน่นอน แน่นอนว่าคอมไพเลอร์บางตัวอาจเสนอส่วนขยายเพื่อหลีกเลี่ยงปัญหานี้เนื่องจากดูเหมือนว่า MSVC ++ 8 และ g ++ เวอร์ชันล่าสุดทำ
j_random_hacker

9
ข้อ จำกัด นี้ถูกยกขึ้นใน C ++ 11
Stephan Dollberg

31

แอปพลิเคชั่นหนึ่งของคลาส C ++ ที่กำหนดในเครื่องอยู่ในรูปแบบการออกแบบจากโรงงาน :


// In some header
class Base
{
public:
    virtual ~Base() {}
    virtual void DoStuff() = 0;
};

Base* CreateBase( const Param& );

// in some .cpp file
Base* CreateBase( const Params& p )
{
    struct Impl: Base
    {
        virtual void DoStuff() { ... }
    };

    ...
    return new Impl;
}

แม้ว่าคุณจะทำเช่นเดียวกันกับเนมสเปซที่ไม่ระบุตัวตนได้


น่าสนใจ! แม้ว่าข้อ จำกัด เกี่ยวกับเทมเพลตที่ฉันพูดถึงจะมีผลบังคับใช้ แต่วิธีนี้รับประกันได้ว่าไม่สามารถสร้างอินสแตนซ์ของ Impl ได้ (หรือพูดถึง!) ยกเว้น CreateBase () ดังนั้นนี่จึงเป็นวิธีที่ยอดเยี่ยมในการลดขอบเขตที่ลูกค้าขึ้นอยู่กับรายละเอียดการใช้งาน +1.
j_random_hacker

26
นั่นเป็นความคิดที่เรียบร้อยไม่แน่ใจว่าฉันจะใช้มันในเร็ว ๆ นี้หรือไม่ แต่อาจเป็นวิธีที่ดีที่จะดึงออกมาที่บาร์เพื่อสร้างความประทับใจให้ลูกไก่ :)
Robert Gould

2
(ฉันกำลังพูดถึงลูกไก่ BTW ไม่ใช่คำตอบ!)
markh44

9
lol โรเบิร์ต ... ใช่ไม่มีอะไรที่ทำให้ผู้หญิงประทับใจเหมือนกับการรู้เกี่ยวกับมุมที่คลุมเครือของ C ++ ...
j_random_hacker

10

จริงๆแล้วมันมีประโยชน์มากสำหรับการทำงานด้านความปลอดภัยที่อิงกับสแต็ก หรือล้างข้อมูลทั่วไปจากฟังก์ชันที่มีจุดส่งคืนหลายจุด สิ่งนี้มักเรียกว่าสำนวน RAII (การได้มาซึ่งทรัพยากรคือ initialzation)

void function()
{

    struct Cleaner
    {
        Cleaner()
        {
            // do some initialization code in here
            // maybe start some transaction, or acquire a mutex or something
        }

        ~Cleaner()
        {
             // do the associated cleanup
             // (commit your transaction, release your mutex, etc.)
        }
    };

    Cleaner cleaner;

    // Now do something really dangerous
    // But you know that even in the case of an uncaught exception, 
    // ~Cleaner will be called.

    // Or alternatively, write some ill-advised code with multiple return points here.
    // No matter where you return from the function ~Cleaner will be called.
}

5
Cleaner cleaner();ฉันคิดว่านี่จะเป็นการประกาศฟังก์ชันมากกว่าการกำหนดวัตถุ
ผู้ใช้

2
@user คุณพูดถูก ที่จะเรียกตัวสร้างเริ่มต้นที่เขาควรจะเขียนหรือCleaner cleaner; Cleaner cleaner{};
callyalater

ฟังก์ชันภายในคลาสไม่มีส่วนเกี่ยวข้องกับ RAII และนอกจากนี้ยังไม่ใช่รหัส C ++ ที่ถูกต้องและจะไม่คอมไพล์
Mikhail Vasilyev

1
แม้แต่ฟังก์ชันภายในคลาสเช่นนี้ก็เป็นสิ่งที่ RAII ใน C ++ เป็นข้อมูลเกี่ยวกับอย่างแม่นยำ
Christopher Bruns

9

โดยพื้นฐานแล้วทำไมไม่? กstructใน C (ย้อนกลับไปในรุ่งอรุณของเวลา) เป็นเพียงวิธีการประกาศโครงสร้างบันทึก ถ้าคุณต้องการทำไมไม่สามารถประกาศได้ในที่ที่คุณจะประกาศตัวแปรง่ายๆ?

เมื่อคุณทำเช่นนั้นโปรดจำไว้ว่าเป้าหมายของ C ++ คือต้องเข้ากันได้กับ C ถ้าเป็นไปได้ทั้งหมด ดังนั้นมันจึงอยู่


เป็นคุณสมบัติที่ดีที่จะมีชีวิตรอด แต่ j_random_hacker เพิ่งชี้ให้เห็นว่ามันไม่มีประโยชน์อย่างที่ฉันจินตนาการไว้ใน C ++: /
Robert Gould

ใช่กฎการกำหนดขอบเขตก็แปลกใน C เช่นกัน ฉันคิดว่าตอนนี้ฉันมีประสบการณ์มากกว่า 25 ปีกับ C ++ แล้วบางทีการพยายามเป็นเหมือน C ให้มากที่สุดอาจเป็นความผิดพลาด ในทางกลับกันภาษาที่หรูหรากว่าเช่น Eiffel ไม่ได้ถูกนำมาใช้เกือบพร้อม
Charlie Martin

ใช่ฉันได้ย้ายฐานข้อมูล C ที่มีอยู่ไปยัง C ++ แล้ว (แต่ไม่ใช่ไปที่ Eiffel)
ChrisW

5

ตัวอย่างเช่นส่วน "7.8: คลาสท้องถิ่น: คลาสภายในฟังก์ชัน" ของhttp://www.icce.rug.nl/documents/cplusplus/cplusplus07.htmlซึ่งเรียกว่า "คลาสท้องถิ่น" และระบุว่า " จะมีประโยชน์มากในแอปพลิเคชันขั้นสูงที่เกี่ยวข้องกับการสืบทอดหรือเทมเพลต "


3

ใช้สำหรับสร้างอาร์เรย์ของวัตถุที่เริ่มต้นอย่างถูกต้อง

ฉันมีคลาส C ซึ่งไม่มีตัวสร้างเริ่มต้น ฉันต้องการอาร์เรย์ของอ็อบเจ็กต์ของคลาส C ฉันคิดว่าฉันต้องการให้อ็อบเจ็กต์เหล่านั้นเริ่มต้นอย่างไรจากนั้นรับคลาส D จาก C ด้วยวิธีการแบบคงที่ซึ่งจัดเตรียมอาร์กิวเมนต์สำหรับ C ในตัวสร้างเริ่มต้นของ D:

#include <iostream>
using namespace std;

class C {
public:
  C(int x) : mData(x)  {}
  int method() { return mData; }
  // ...
private:
  int mData;
};

void f() {

  // Here I am in f.  I need an array of 50 C objects starting with C(22)

  class D : public C {
  public:
    D() : C(D::clicker()) {}
  private:
    // I want my C objects to be initialized with consecutive
    // integers, starting at 22.
    static int clicker() { 
      static int current = 22;
      return current++;
    } 
  };

  D array[50] ;

  // Now I will display the object in position 11 to verify it got initialized
  // with the right value.  

  cout << "This should be 33: --> " << array[11].method() << endl;

  cout << "sizodf(C): " << sizeof(C) << endl;
  cout << "sizeof(D): " << sizeof(D) << endl;

  return;

}

int main(int, char **) {
  f();
  return 0;
}

เพื่อความเรียบง่ายตัวอย่างนี้ใช้ตัวสร้างที่ไม่ใช่ค่าเริ่มต้นเล็กน้อยและกรณีที่ทราบค่าในเวลาคอมไพล์ เป็นเรื่องง่ายที่จะขยายเทคนิคนี้ไปยังกรณีที่คุณต้องการให้อาร์เรย์ของอ็อบเจ็กต์เริ่มต้นด้วยค่าที่ทราบเฉพาะในรันไทม์เท่านั้น


เป็นแอปพลิเคชั่นที่น่าสนใจอย่างแน่นอน! ไม่แน่ใจว่ามันฉลาดหรือปลอดภัยแม้ว่า - หากคุณต้องการถือว่าอาร์เรย์ของ D นั้นเป็นอาร์เรย์ของ C (เช่นคุณต้องส่งต่อไปยังฟังก์ชันที่รับD*พารามิเตอร์) สิ่งนี้จะแตกโดยเงียบหาก D มีขนาดใหญ่กว่า C . (ฉันคิดว่า ... )
j_random_hacker

+ j_random_hacker, sizeof (D) == sizeof (C) ฉันเพิ่มรายงาน sizeof () ให้คุณแล้ว
Thomas L Holaday
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.