【问题标题】:Compile-time constant id编译时常量 id
【发布时间】:2011-11-25 14:24:57
【问题描述】:

鉴于以下情况:

template<typename T>
class A
{
public:
    static const unsigned int ID = ?;
};

我希望 ID 为每个 T 生成一个唯一的编译时 ID。我考虑过 __COUNTER__ 和 boost PP 库,但到目前为止都没有成功。我怎样才能做到这一点?

编辑:ID 必须可用作 switch 语句中的大小写

Edit2:所有基于静态方法或成员地址的答案都不正确。尽管它们确实创建了一个唯一 ID,但它们在编译时不会被解析,因此不能用作 switch 语句的情况。

【问题讨论】:

  • 如果你能设法找到一个编译时散列函数,请对__FILE____LINE__ 进行散列。
  • ids 是否需要从零开始线性增加?还是只是唯一的 ID?
  • @tenfour:对于所有T,这将得到相同的评估。
  • 另外,为什么需要这个编译时计数器?
  • 你想做什么?您是否只是在寻找基于不同类型名的 A 类的“唯一性”?还是独一无二的,包括后续的编译?例如 A 每次编译都会有一个 diff id?很好奇为什么它在案例陈述中如此重要?

标签: c++ templates template-meta-programming


【解决方案1】:

假设一个符合标准的编译器就足够了(关于一个定义规则):

template<typename T>
class A
{
public:
    static char ID_storage;
    static const void * const ID;
};

template<typename T> char A<T>::ID_storage;
template<typename T> const void * const A<T>::ID= &A<T>::ID_storage;

来自 C++ 标准 3.2.5 一个定义规则 [basic.def.odr](粗体强调我的):

... 如果 D 是一个模板并且在多个翻译中定义 单位,则上面列表中的最后四个要求应适用 到模板中使用的模板封闭范围的名称 定义(14.6.3),以及在点的从属名称 实例化(14.6.2)。 如果 D 的定义满足所有这些 要求,那么程序的行为就好像有一个单一的 D 的定义。 如果 D 的定义不满足这些 要求,则行为未定义。

【讨论】:

  • 我会将其设为 void * - 您只对相等比较感兴趣,因此无需公开您指向 chars 的实现细节
  • 是不够的,每个编译单元都会得到自己的值的副本。为了工作,它们应该在 CPP 中根据一个定义规则进行定义,但由于它们是模板,因此无法完成。
  • @bdonlan:为了什么?单一定义规则?或者在头文件中定义的静态对象将在包含它们的每个翻译单元中定义这一事实?请记住,#include 与复制和粘贴文件内容没有什么不同...
  • @MSN:很好……但在编译时不可用。 AFAIK 编译时生成唯一 ID 是不可能的。请注意,使用指向私有静态函数的指针(有点像 Safe Bool 习惯用法)同样有效,但仍然无法回答问题。
  • 澄清一下:是否每个人都同意,给定函数/方法在许多翻译单元中的多个定义,链接器必须准确选择一个在运行时使用? (也许还有其他限制,但孤立起来这很有趣)
【解决方案2】:

我通常用的是这个:

template<typename>
void type_id(){}

using type_id_t = void(*)();

由于函数的每个实例都有自己的地址,您可以使用该地址来识别类型:

// Work at compile time
constexpr type_id_t int_id = type_id<int>;

// Work at runtime too
std::map<type_id_t, std::any> types;

types[type_id<int>] = 4;
types[type_id<std::string>] = "values"s

// Find values
auto it = types.find(type_id<int>);

if (it != types.end()) {
    // Found it!
}

【讨论】:

  • 这不是编译时间
  • 函数的地址在编译时是已知的。所以是的,它在运行时与 std::map 一起工作,但你也可以在 constexpr 函数中使用它。
  • @David constexpr 意味着它肯定在编译时
  • 我试过这个,但无法通过“cannot use const void * in a switch statement”。
  • 它可能无法在 Windows 上运行。经过一些 Visual Studio 优化后,函数的地址可以相同。见:github.com/gelldur/EventBus/issues/19
【解决方案3】:

这对我来说似乎没问题:

template<typename T>
class Counted
{
  public:
  static int id()
  {
    static int v;
    return (int)&v;
  }
};

#include <iostream>

int main()
{
  std::cout<<"Counted<int>::id()="<<Counted<int>::id()<<std::endl;
  std::cout<<"Counted<char>::id()="<<Counted<char>::id()<<std::endl;

}

【讨论】:

  • 这适用于您的用例,现在将 Counted 的定义移动到它自己的标题中,并从不同的翻译单元中包含它。
  • @K-ballo 对于我的带有多个翻译单元的简单测试用例,似乎可以正常工作。你有失败的案例吗?
  • 嗯,实际上这种方法似乎有效。该标准保证所有翻译单元都指向同一个v 对象。
  • 我认为 id() 的结果是在运行时评估的。
  • @Dave - 是的,它不是编译时间常数。我错过了问题中的那部分。
【解决方案4】:

可以使用this answer 中的代码从字符串生成编译时HASH。

如果您可以修改模板以包含一个额外的整数并使用宏来声明变量:

template<typename T, int ID> struct A
{
    static const int id = ID;
};

#define DECLARE_A(x) A<x, COMPILE_TIME_CRC32_STR(#x)>

使用此宏进行类型声明,id 成员包含类型名称的散列。例如:

int main() 
{
    DECLARE_A(int) a;
    DECLARE_A(double) b;
    DECLARE_A(float) c;
    switch(a.id)
    {
    case DECLARE_A(int)::id:
        cout << "int" << endl;
        break;
    case DECLARE_A(double)::id:
        cout << "double" << endl;
        break;
    case DECLARE_A(float)::id:
        cout << "float" << endl;
        break;
    };
    return 0;
}

当类型名称转换为字符串时,对类型名称文本的任何修改都会导致不同的 id。例如:

static_assert(DECLARE_A(size_t)::id != DECLARE_A(std::size_t)::id, "");

另一个缺点是可能发生哈希冲突。

【讨论】:

【解决方案5】:

使用静态函数的内存地址。

template<typename T>
class A  {
public:
    static void ID() {}
}; 

(&amp;(A&lt;int&gt;::ID)) 将不同于 (&amp;(A&lt;char&gt;::ID)) 等等。

【讨论】:

  • 编译器报错:“‘ID’没有在这个范围内声明”+“‘&’不能出现在常量表达式中”
  • 为了获取静态常量积分的地址(注意您缺少 const 修饰符),必须在适当的 CPP 文件中定义此类积分,而模板无法完成。否则是未定义的行为;如果静态在头文件中声明,您将在不同的编译单元获得不同的地址。
  • C++ 标准真的能保证整数的这一点吗?这至少在 64 位平台上肯定会失败
  • 另外,C++ 标准是否保证模板函数指针会像这样比较相等?
  • @bdonlan:实际上这并不能保证,因为它会在不同的编译单元中获得不同的地址。此外,即使在同一个编译单元中,像 MSVC 这样的编译器也会拒绝这样做,因为它们会优化所有函数以指向同一个。
【解决方案6】:

我最近遇到了这个确切的问题。 我的解决方案:

counter.hpp

class counter
{
    static int i;
    static nexti()
    {
        return i++;
    }
};

Counter.cpp:

int counter::i = 0;

模板类.hpp

#include "counter.hpp"

    template <class T>
    tclass
    {
        static const int id;
    };

    template <class T>
    int tclass<T>::id = counter::nexti();

它可以在 MSVC 和 GCC 中正常工作,除了你不能在 switch 语句中使用它。

出于各种原因,我实际上更进一步,并定义了一个预处理器宏,该宏从给定名称参数创建一个新类,该类具有派生自公共基础的静态 ID(如上)。

【讨论】:

    【解决方案7】:

    使用this常量表达式计数器:

    template <class T>
    class A
    {
    public:
        static constexpr int ID() { return next(); }
    };
    class DUMMY { };
    int main() {
        std::cout << A<char>::ID() << std::endl;
        std::cout << A<int>::ID() << std::endl;
        std::cout << A<BETA>::ID() << std::endl;
        std::cout << A<BETA>::ID() << std::endl;
        return 0;
    }
    

    输出:(GCC,C++14)

    1
    2
    3
    3
    

    缺点是您需要猜测派生类数量的上限,常量表达式计数器才能工作。

    【讨论】:

      【解决方案8】:

      这是一个主要基于模板的可能解决方案:

      #include<cstddef>
      #include<functional>
      #include<iostream>
      
      template<typename T>
      struct wrapper {
          using type = T;
          constexpr wrapper(std::size_t N): N{N} {}
          const std::size_t N;
      };
      
      template<typename... T>
      struct identifier: wrapper<T>... {
          template<std::size_t... I>
          constexpr identifier(std::index_sequence<I...>): wrapper<T>{I}... {}
      
          template<typename U>
          constexpr std::size_t get() const { return wrapper<U>::N; }
      };
      
      template<typename... T>
      constexpr identifier<T...> ID = identifier<T...>{std::make_index_sequence<sizeof...(T)>{}};
      
      // ---
      
      struct A {};
      struct B {};
      
      constexpr auto id = ID<A, B>;
      
      int main() {
          switch(id.get<B>()) {
          case id.get<A>():
              std::cout << "A" << std::endl;
              break;
          case id.get<B>():
              std::cout << "B" << std::endl;
              break;
          }
      }
      

      请注意,这需要 C++14。

      将顺序 ID 关联到类型列表所需要做的就是将该列表提供给模板变量,如上例所示:

      constexpr auto id = ID<A, B>;
      

      从那时起,您可以通过get 方法获取给定类型的给定ID:

      id.get<A>()
      

      仅此而已。您可以根据要求在switch 语句中使用它,如示例代码所示。

      请注意,只要将类型附加到与数字 id 相关联的类列表中,标识符在每次编译后和每次执行期间都是相同的。
      如果你想从列表中删除一个类型,你仍然可以使用 fake 类型作为占位符,例如:

      template<typename> struct noLonger { };
      constexpr auto id = ID<noLonger<A>, B>;
      

      这将确保 A 不再有关联的 id,并且分配给 B 的 ID 不会改变。
      如果你不想明确删除A,你可以使用类似的东西:

      constexpr auto id = ID<noLonger<void>, B>;
      

      或其他。

      【讨论】:

      • constexpr auto id = ID&lt;A, B&gt;; 所以你必须知道每个可能的类型,你想要一个编译时 ID 并列出它们?
      • @David 嗯,是的,你有。如果您想在编译时为它们分配一个 id,这是一种隐含的要求。如果您以某种方式列出它们,或者如果您决定将其基于 CRTP 习语,那么您就是在明确标记您的类型。
      • @David 添加了更多详细信息。希望对您有所帮助。
      【解决方案9】:

      好的.....所以这是我从this 网站发现的一个黑客。它应该工作。您唯一需要做的就是向您的struct 添加另一个模板参数,该参数采用计数器“元对象”。请注意,Aintboolchar 都有唯一的 ID,但不能保证 int 的 ID 将是 1bool 将是 2 等。 ,因为模板启动的顺序不一定是已知的。

      另一个说明:

      这不适用于 Microsoft Visual C++

      #include <iostream>
      #include "meta_counter.hpp"
      
      template<typename T, typename counter>
      struct A
      {
          static const size_t ID = counter::next();
      };
      
      int main () {
          typedef atch::meta_counter<void> counter;
          typedef A<int,counter> AInt;
          typedef A<char,counter> AChar;
          typedef A<bool,counter> ABool;
          switch (ABool::ID)
          {
              case AInt::ID:
                  std::cout << "Int\n";
                  break;
              case ABool::ID:
                  std::cout << "Bool\n";
                  break;
              case AChar::ID:
                  std::cout << "Char\n";
                  break;
          }
      
          std::cout << AInt::ID << std::endl;
          std::cout << AChar::ID << std::endl;
          std::cout << ABool::ID << std::endl;
          std::cout << AInt::ID << std::endl;
          while (1) {}
      }
      

      这里是meta_counter.hpp

      // author: Filip Roséen <filip.roseen@gmail.com>
      // source: http://b.atch.se/posts/constexpr-meta-container
      
      #ifndef ATCH_META_COUNTER_HPP
      #define ATCH_META_COUNTER_HPP
      
      #include <cstddef>
      
      namespace atch { namespace {
      
        template<class Tag>
        struct meta_counter {
          using size_type = std::size_t;
      
          template<size_type N>
          struct ident {
            friend constexpr size_type adl_lookup (ident<N>);
            static constexpr size_type value = N;
          };
      
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
      
          template<class Ident>
          struct writer {
            friend constexpr size_type adl_lookup (Ident) {
              return Ident::value;
            }
      
            static constexpr size_type value = Ident::value;
          };
      
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
      
          template<size_type N, int = adl_lookup (ident<N> {})>
          static constexpr size_type value_reader (int, ident<N>) {
            return N;
          }
      
          template<size_type N>
          static constexpr size_type value_reader (float, ident<N>, size_type R = value_reader (0, ident<N-1> ())) {
            return R;
          }
      
          static constexpr size_type value_reader (float, ident<0>) {
            return 0;
          }
      
          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
      
          template<size_type Max = 64>
          static constexpr size_type value (size_type R = value_reader (0, ident<Max> {})) {
            return R;
          }
      
          template<size_type N = 1, class H = meta_counter>
          static constexpr size_type next (size_type R = writer<ident<N + H::value ()>>::value) {
            return R;
          }
        };
      }}
      
      #endif /* include guard */
      

      【讨论】:

      • 问题:为什么A有一个模板参数counter?难道不能直接使用meta_counter&lt;void&gt;吗? meta_counter Tag 的唯一目的是支持多重计数吗?
      • @David Tag 参数的唯一目的是支持多个计数(就像我说我没有写很多这样的代码)。不,如果您只有meta_counter&lt;void&gt;,所有类都具有相同的 ID(不过我最初尝试过)。也就是说,您可能会做一些模板技巧,以便在需要时这样做是透明的。
      • innterrestttinnnng。我将不得不阅读为什么将meta_counter 不作为模板参数不起作用。这对我来说没有多大意义。关于模板实例化?
      • @David 我不知道这个问题的答案。我所做的只是做了一点实验。
      • 复制并粘贴您的代码,它似乎不起作用。所有的 ID 都是一样的。使用 clang/xcode 8/c++14
      【解决方案10】:

      使用模板,如果是constexpr,需要c++17

      #include <iostream>
      
      template <typename Type, typename... Types>
      struct TypeRegister{
          template<typename Queried_type>
          static constexpr int id(){
              if constexpr (std::is_same_v<Type, Queried_type>) return 0;
              else{
                  static_assert((sizeof...(Types) > 0), "You shan't query a type you didn't register first");
                  return 1 + TypeRegister<Types...>::template id<Queried_type>();
              }
          }
      };
      
      int main(){
          using reg_map = TypeRegister<int, float, char, const int&>;
          std::cout << reg_map::id<const int&>() << std::endl;// 3
          // std::cout << reg_map::id<const int>() << std::endl;// error
      }
      

      【讨论】:

        【解决方案11】:

        这是做不到的。静态对象的地址是您可以获得的最接近唯一 ID 的地址,但是为了获取此类对象的地址(甚至是静态 const 积分),它们必须在某处定义。根据一个定义规则,它们应该在 CPP 文件中定义,因为它们是模板,所以不能这样做。如果你在头文件中定义静态,那么每个编译单元都会得到它自己的版本,当然在不同的地址实现。

        【讨论】:

        • 我刚刚发布了一个可以做到这一点的答案
        【解决方案12】:

        几个月前我遇到了类似的问题。我一直在寻找一种技术来定义每次执行时都相同的标识符。
        如果这是一个要求,here 是另一个探索或多或少相同问题的问题(当然,它带有很好的答案)。
        无论如何,我没有使用建议的解决方案。它遵循了我当时所做的事情的描述。


        您可以定义一个constexpr 函数,如下所示:

        static constexpr uint32_t offset = 2166136261u;
        static constexpr uint32_t prime = 16777619u;
        
        constexpr uint32_t fnv(uint32_t partial, const char *str) {
            return str[0] == 0 ? partial : fnv((partial^str[0])*prime, str+1);
        }
        
        inline uint32_t fnv(const char *str) {
            return fnv(offset, str);
        }
        

        然后是这样的类来继承:

        template<typename T>
        struct B {
            static const uint32_t id() {
                static uint32_t val = fnv(T::identifier);
                return val;
            }
        };
        

        CRTP 成语完成了剩下的工作。
        例如,您可以如下定义派生类:

        struct C: B<C> {
            static const char * identifier;
        };
        
        const char * C::identifier = "ID(C)";
        

        只要为不同的类提供不同的标识符,就会有唯一的数值,可以用来区分类型。

        标识符不需要是派生类的一部分。例如,您可以通过 trait 提供它们:

        template<typename> struct trait;
        template<> struct trait { static const char * identifier; };
        
        // so on with all the identifiers
        
        template<typename T>
        struct B {
            static const uint32_t id() {
                static uint32_t val = fnv(trait<T>::identifier);
                return val;
            }
        };
        

        优点:

        • 易于实施。
        • 没有依赖关系。
        • 每次执行期间数值都相同。
        • 如果需要,类可以共享相同的数字标识符。

        缺点:

        • 容易出错:复制粘贴很快就会成为您最大的敌人。

        它遵循上面描述的一个最小的工作示例。
        我修改了代码以便能够在switch 语句中使用ID 成员方法:

        #include<type_traits>
        #include<cstdint>
        #include<cstddef>
        
        static constexpr uint32_t offset = 2166136261u;
        static constexpr uint32_t prime = 16777619u;
        
        template<std::size_t I, std::size_t N>
        constexpr
        std::enable_if_t<(I == N), uint32_t>
        fnv(uint32_t partial, const char (&)[N]) {
            return partial;
        }
        
        template<std::size_t I, std::size_t N>
        constexpr
        std::enable_if_t<(I < N), uint32_t>
        fnv(uint32_t partial, const char (&str)[N]) {
            return fnv<I+1>((partial^str[I])*prime, str);
        }
        
        template<std::size_t N>
        constexpr inline uint32_t fnv(const char (&str)[N]) {
            return fnv<0>(offset, str);
        }
        
        template<typename T>
        struct A {
            static constexpr uint32_t ID() {
                return fnv(T::identifier);
            }
        };
        
        struct C: A<C> {
            static constexpr char identifier[] = "foo";
        };
        
        struct D: A<D> {
            static constexpr char identifier[] = "bar";
        };
        
        int main() {
            constexpr auto val = C::ID();
        
            switch(val) {
            case C::ID():
                break;
            case D::ID():
                break;
            default:
                break;
            }
        }
        

        请注意,如果您想在非常量表达式中使用ID,您必须在某处定义identifiers,如下所示:

        constexpr char C::identifier[];
        constexpr char D::identifier[];
        

        一旦你这样做了,你就可以这样做:

        int main() {
            constexpr auto val = C::ID();
            // Now, it is well-formed
            auto ident = C::ID();
        
            // ...
        }
        

        【讨论】:

        • 如果您要求每个 T 都有一个唯一的 identifier 静态成员字符串,那就错了 - 它们每个都已经有一个唯一的编译时间标识符
        • @David 如果您的要求是有一个数字标识符(比较整数与比较字符串之间的差异,可能我不必提供有关此的更多详细信息),但您仍然想使用方便且简单的使用在编译时丢弃的标识符......等等!
        【解决方案13】:

        这是一个 C++ 代码,它使用 __DATE____TIME__ 宏来获取类型 &lt;T&gt; 的唯一标识符

        格式:

        // __DATE__ "??? ?? ????"
        // __TIME__ "??:??:??"
        

        这是一个质量很差的哈希函数:

        #define HASH_A 8416451
        #define HASH_B 11368711
        #define HASH_SEED 9796691    \
        + __DATE__[0x0] * 389        \
        + __DATE__[0x1] * 82421      \
        + __DATE__[0x2] * 1003141    \
        + __DATE__[0x4] * 1463339    \
        + __DATE__[0x5] * 2883371    \
        + __DATE__[0x7] * 4708387    \
        + __DATE__[0x8] * 4709213    \
        + __DATE__[0x9] * 6500209    \
        + __DATE__[0xA] * 6500231    \
        + __TIME__[0x0] * 7071997    \
        + __TIME__[0x1] * 10221293   \
        + __TIME__[0x3] * 10716197   \
        + __TIME__[0x4] * 10913537   \
        + __TIME__[0x6] * 14346811   \
        + __TIME__[0x7] * 15485863
        
        unsigned HASH_STATE = HASH_SEED;
        unsigned HASH() {
            return HASH_STATE = HASH_STATE * HASH_A % HASH_B;
        }
        

        使用哈希函数:

        template <typename T>
        class A
        {
        public:
            static const unsigned int ID;
        };
        
        template <>
        const unsigned int A<float>::ID = HASH();
        
        template <>
        const unsigned int A<double>::ID = HASH();
        
        template <>
        const unsigned int A<int>::ID = HASH();
        
        template <>
        const unsigned int A<short>::ID = HASH();
        
        #include <iostream>
        
        int main() {
            std::cout << A<float>::ID << std::endl;
            std::cout << A<double>::ID << std::endl;
            std::cout << A<int>::ID << std::endl;
            std::cout << A<short>::ID << std::endl;
        }
        

        【讨论】:

          【解决方案14】:

          如果可以接受非单调值和intptr_t

          template<typename T>
          struct TypeID
          {
          private:
              static char id_ref;
          public:
              static const intptr_t ID;
          };
          
          template<typename T>
            char TypeID<T>::id_ref;
          template<typename T>
            const intptr_t TypeID<T>::ID = (intptr_t)&TypeID<T>::id_ref;
          

          如果你必须有整数,或者必须有单调递增的值,我认为使用静态构造函数是唯一的方法:

          // put this in a namespace
          extern int counter;
          
          template<typename T>
          class Counter {
          private:
            Counter() {
              ID_val = counter++;
            }
            static Counter init;
            static int ID_val;
          public:
            static const int &ID;
          };
          
          template<typename T>
            Counter<T> Counter<T>::init;
          template<typename T>
            int Counter<T>::ID_val;
          template<typename T>
            const int &Counter<T>::ID = Counter<T>::ID_val;
          
          // in a non-header file somewhere
          int counter;
          

          请注意,如果您在共享库和您的应用程序之间共享这些技术,它们都不安全!

          【讨论】:

          • 您能否解释一下共享库(dll、lib 或其他)的含义以及为什么在它们之间共享不安全。
          【解决方案15】:

          另一种选择是考虑以下具有唯一静态成员字段type 的类Data

          template <class T>
          class Data
          {
          public:
              static const std::type_index type;
          };
          // do [static data member initialization](http://stackoverflow.com/q/11300652/3041008)
          // by [generating unique type id](http://stackoverflow.com/q/26794944/3041008)
          template <class T>
          std::type_index const Data<T>::type = std::type_index(typeid(T));
          

          产生输出 (MinGWx64-gcc4.8.4 -std=c++11 -O2)

          printf("%s %s\n", Data<int>::type.name(), Data<float>::type.name())
          //prints "i f"
          

          它不完全是整数 id 或可打印的字符串,也不是 constexpr,但可以是 used as an index in (un)ordered associative containers
          如果Data.h 标头包含在多个文件中(相同的hashCode() 值),它似乎也可以工作。

          【讨论】:

          • 对,引用你自己的话:“所有基于[静态初始化]的答案都是不正确的。虽然它们确实创建了一个唯一的ID,但它们在编译时没有解析,因此不能使用作为 switch 语句的情况"
          【解决方案16】:

          这是一个实用的解决方案,如果您可以为要使用的每个 type 编写单独的一行 DECLARE_ID(type)

           #include <iostream>
          
           template<class> struct my_id_helper;
           #define DECLARE_ID(C) template<> struct my_id_helper<C> { enum {value = __COUNTER__ }; }
          
           // actually declare ids:
           DECLARE_ID(int);
           DECLARE_ID(double);
           // this would result in a compile error: redefinition of struct my_id_helper<int>’
           // DECLARE_ID(int);
          
           template<class T>
           class A
           {
           public:
               static const unsigned int ID = my_id_helper<T>::value;
           };
          
           int main()
           {
               switch(A<int>::ID)
               {
               case A<int>::ID:    std::cout << "it's an int!\n"; break;
               case A<double>::ID: std::cout << "it's a double!\n"; break;
               // case A<float>::ID: // error: incomplete type ‘my_id_helper<float>’
               default: std::cout << "it's something else\n"; break;
               }
           }
          

          【讨论】:

            【解决方案17】:
            template<typename T>
            static void get_type_id() { void* x; new (x) T(); }
            using type_id_t = void(*)();
            

            优化效果很好

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-12-26
              • 1970-01-01
              • 1970-01-01
              • 2012-02-23
              • 2014-09-03
              • 2012-06-13
              相关资源
              最近更新 更多